import XCTest /// Drives the App Store preview's shots while the simulator records (see design/video). Skipped /// unless the runner sets PROMO_VIDEO=1 (`TEST_RUNNER_PROMO_VIDEO=1 xcodebuild test …`). Each shot /// prints when it starts and ends, so the recording can be cut to exactly that span. @MainActor final class PromoVideoUITests: XCTestCase { private func launch(_ extra: [String]) throws -> XCUIApplication { try XCTSkipUnless(ProcessInfo.processInfo.environment["PROMO_VIDEO"] == "1", "Promo video shots run on request") continueAfterFailure = false let app = XCUIApplication() app.launchArguments = XCUIApplication.chatsSection + XCUIApplication.straightToChat + ["-echo.demoHosts", "-transport", "chatCompletions"] + extra app.launch() return app } private func mark(_ what: String) { print("PROMO \(what) \(Date().timeIntervalSince1970)") } private func hold(_ seconds: Double) { RunLoop.current.run(until: Date().addingTimeInterval(seconds)) } private func drag(_ app: XCUIApplication, from: CGVector, to: CGVector, velocity: XCUIGestureVelocity = .slow) { let w = app.windows.firstMatch w.coordinate(withNormalizedOffset: from) .press(forDuration: 0.05, thenDragTo: w.coordinate(withNormalizedOffset: to), withVelocity: velocity, thenHoldForDuration: 0.1) } // MARK: BOTS! (design/video, `make_preview.py bots`) /// The demo bots in the Code theme, dark, as every BOTS! take is. private func bots(_ extra: [String]) throws -> XCUIApplication { try launch(["-echo.demoProjects", "-echo.demoBots", "-echo.clearDrafts", "-displayName", "Redde", "-theme", "claudeCode", "-appearance", "dark"] + extra) } /// New Bot from a template: Researcher, then Writer, then Coder fill it in; then its face, /// changed with Surprise Me. func testBotsTemplate() throws { let app = try bots(["-echo.screen", "sessions", "-echo.botEditor", "new"]) let researcher = app.buttons["Researcher"].firstMatch XCTAssertTrue(researcher.waitForExistence(timeout: 20)) mark("start") hold(1.2) for name in ["Researcher", "Writer", "Coder"] { app.buttons[name].firstMatch.tap() mark(name.lowercased()) hold(1.7) } app.buttons["Change Face"].firstMatch.tap() XCTAssertTrue(app.staticTexts["Shape"].firstMatch.waitForExistence(timeout: 5)) mark("faces") hold(1.0) // The parts at the top of the picker, each drawn as the bot with it. for part in ["Star", "Blob", "Ghost", "Sparkle", "Happy"] { let button = app.buttons[part].firstMatch if button.exists { button.tap(); hold(0.75) } } mark("end") hold(1.0) } /// In a bot's chat, Scout and Quill having messaged each other; then an @ offers the other /// bots, and one picked, the question goes on. func testBotsMention() throws { let app = try bots(["-echo.demoMarks"]) XCTAssertTrue(app.text(containing: "Lost Lake").waitForExistence(timeout: 20)) let field = app.textFields.firstMatch XCTAssertTrue(field.waitForExistence(timeout: 10)) mark("start") hold(1.0) field.tap() hold(0.5) field.typeText("Ask @") let scout = app.buttons.containing(NSPredicate(format: "label CONTAINS %@", "@scout")).firstMatch XCTAssertTrue(scout.waitForExistence(timeout: 5)) mark("offered") hold(1.6) scout.tap() field.typeText("which trails are open this weekend") mark("end") hold(1.5) } /// The group's voice screen: one question, and Redde, Scout and Quill answering it in turn, /// each in its own voice (Kokoro's, at `TEST_RUNNER_KOKORO_URL`, when given). Marks as each begins. func testBotsVoiceRound() throws { var voice: [String] = [] if let url = ProcessInfo.processInfo.environment["KOKORO_URL"], !url.isEmpty { voice = ["-replyVoice", "kokoro", "-kokoroURL", url, "-voiceSpeed", "1"] } let app = try bots(["-echo.screen", "sessions", "-conversations.section", "sessions", "-mutes", "<>", "-echo.openGroup", "-echo.demoRound", "-echo.groupVoice"] + voice) var label: XCUIElement? for name in ["Redde", "Scout", "Quill"] { label = app.staticTexts.matching(NSPredicate(format: "label ==[c] %@", "\(name) is speaking")).firstMatch XCTAssertTrue(label!.waitForExistence(timeout: 40), "\(name) never spoke") mark(name.lowercased()) } if let label { wait(for: [XCTNSPredicateExpectation(predicate: NSPredicate(format: "exists == false"), object: label)], timeout: 30) } mark("end") hold(1.5) } /// The 2.0 store screenshots' raw shots, as pictures in SHOT_DIR (`ph-` or `pad-` by the /// device; design/screenshots): the chat list's Bots, and a group chat's voice screen once the /// round has come to Quill, the third bot, with the others' answers above it. func testBotsShots() throws { let dir = URL(fileURLWithPath: try XCTUnwrap(ProcessInfo.processInfo.environment["SHOT_DIR"], "SHOT_DIR is where they go")) let kind = UIDevice.current.userInterfaceIdiom == .pad ? "pad" : "ph" var voice: [String] = [] if let url = ProcessInfo.processInfo.environment["KOKORO_URL"], !url.isEmpty { voice = ["-replyVoice", "kokoro", "-kokoroURL", url, "-voiceSpeed", "1"] } // The iPad's sidebar shows the library under the bots: the sample one, as its other shots. let screen = kind == "ph" ? ["-echo.screen", "sessions"] : ["-echo.demoLibrary"] let list = try bots(screen + ["-conversations.section", "sessions", "-echo.demoMarks", "-mutes", "<>"]) XCTAssertTrue(list.staticTexts["Weekend hike"].firstMatch.waitForExistence(timeout: 20)) hold(3) try XCUIScreen.main.screenshot().pngRepresentation.write(to: dir.appending(path: "\(kind)-bots.png")) let room = try bots(screen + ["-conversations.section", "sessions", "-mutes", "<>", "-echo.openGroup", "-echo.demoRound", "-echo.groupVoice"] + voice) let quill = room.staticTexts.matching(NSPredicate(format: "label ==[c] %@", "Quill is speaking")).firstMatch XCTAssertTrue(quill.waitForExistence(timeout: 60), "the round never came to Quill") hold(1.5) try XCUIScreen.main.screenshot().pngRepresentation.write(to: dir.appending(path: "\(kind)-crew.png")) } /// Scout muted from its row, then its schedules, one opened. func testBotsMuteAndSchedules() throws { let app = try bots(["-echo.screen", "sessions", "-mutes", "<>"]) let list = app.collectionViews.firstMatch let scout = list.buttons["Scout"].firstMatch XCTAssertTrue(scout.waitForExistence(timeout: 20)) mark("start") hold(1.0) scout.press(forDuration: 1.0) app.buttons["Mute"].firstMatch.tap() hold(0.6) app.buttons["For 8 Hours"].firstMatch.tap() mark("muted") hold(1.8) scout.press(forDuration: 1.0) app.buttons["Schedules…"].firstMatch.tap() let wear = app.buttons.containing(NSPredicate(format: "label CONTAINS %@", "What to wear")).firstMatch XCTAssertTrue(wear.waitForExistence(timeout: 10)) mark("schedules") hold(1.8) wear.tap() mark("opened") hold(2.2) mark("end") } /// Answers with substance: read up through the checklist, table, diagram and math. func testRichAnswers() throws { let app = try launch(["-echo.demo", "-echo.demoLibrary", "-theme", "claudeCode", "-appearance", "dark"]) XCTAssertTrue(app.staticTexts["Where are we on the kitchen project?"].waitForExistence(timeout: 20)) // On iPad the transcript is the right-hand column, beside the sidebar. let x: CGFloat = app.windows.firstMatch.frame.width > 600 ? 0.66 : 0.5 // Diagrams and math draw when they first scroll into view: pass over them all once, then // start the take from the top of the transcript. for _ in 0..<4 { drag(app, from: CGVector(dx: x, dy: 0.8), to: CGVector(dx: x, dy: 0.2), velocity: .fast); hold(0.8) } hold(1.5) for _ in 0..<5 { drag(app, from: CGVector(dx: x, dy: 0.25), to: CGVector(dx: x, dy: 0.85), velocity: .fast) } hold(1.5) mark("start") hold(0.8) for _ in 0..<3 { drag(app, from: CGVector(dx: x, dy: 0.78), to: CGVector(dx: x, dy: 0.38), velocity: 250) hold(0.5) } hold(0.8) mark("end") } /// Every chat, cron job and board: swipe the conversations in, then open the Kanban board. func testConversationsAndKanban() throws { let app = try launch(["-echo.demo", "-echo.demoLibrary", "-echo.demoKanban", "-theme", "claudeCode", "-appearance", "dark"]) XCTAssertTrue(app.buttons["Conversations"].waitForExistence(timeout: 20)) hold(2.5) mark("start") hold(0.7) drag(app, from: CGVector(dx: 0.08, dy: 0.5), to: CGVector(dx: 0.85, dy: 0.52), velocity: 700) hold(1.6) // By position: the Kanban segment in the panel's first row. (Accessibility lookups found // the hidden ⌘3 shortcut button of the same name, or nothing.) app.windows.firstMatch.coordinate(withNormalizedOffset: CGVector(dx: 0.69, dy: 0.262)).tap() hold(2.4) mark("end") } /// Phone tools: the agent reads the calendar, the phone asks before the reminder is added, /// and a photo is found by what it shows (`-echo.demoPhone`). One take, with marks at the /// question's arrival and at its answer. func testPhoneTools() throws { let app = try launch(["-echo.demoPhone", "-theme", "claudeCode", "-appearance", "dark"]) XCTAssertTrue(app.navigationBars.buttons["New conversation"].firstMatch.waitForExistence(timeout: 20)) mark("start") let allow = app.alerts.buttons["Allow"] XCTAssertTrue(allow.waitForExistence(timeout: 20), "Phone tools didn't ask about the reminder") mark("asked") hold(2.0) allow.tap() mark("allowed") XCTAssertTrue(app.staticTexts.containing(NSPredicate(format: "label CONTAINS %@", "bowled")).firstMatch.waitForExistence(timeout: 30)) hold(3.0) mark("end") } /// Phone tools reaching a person: a call the agent starts and a message it writes /// (`-echo.demoPhone reach`). In a simulator there is no phone for iOS to ask about and no /// Messages to open, so the take shows the conversation and not those two screens of iOS's. func testPhoneToolsReach() throws { let app = try launch(["-echo.demoPhone", "reach", "-theme", "claudeCode", "-appearance", "dark"]) XCTAssertTrue(app.navigationBars.buttons["New conversation"].firstMatch.waitForExistence(timeout: 20)) mark("start") XCTAssertTrue(app.staticTexts.containing(NSPredicate(format: "label CONTAINS %@", "Tap Call")).firstMatch.waitForExistence(timeout: 30)) mark("called") XCTAssertTrue(app.staticTexts.containing(NSPredicate(format: "label CONTAINS %@", "You sent it")).firstMatch.waitForExistence(timeout: 30)) mark("sent") hold(2.5) mark("end") } // MARK: The second Phone tools video: a day with the agent (`-echo.demoFun`) private func fun(_ scene: String, _ extra: [String] = []) throws -> XCUIApplication { try launch(["-echo.demoFun", scene, "-theme", "claudeCode", "-appearance", "dark"] + extra) } private func says(_ app: XCUIApplication, _ words: String) -> Bool { app.staticTexts.containing(NSPredicate(format: "label CONTAINS %@", words)).firstMatch.waitForExistence(timeout: 30) } private func started(_ app: XCUIApplication) { XCTAssertTrue(app.navigationBars.buttons["New conversation"].firstMatch.waitForExistence(timeout: 20)) mark("start") } /// Readies a simulator for the takes, unrecorded: iOS asked for Alarms and answered, and the /// agent's widget on the Home Screen. Wants an iOS 26.3 simulator (27.0's refuses alarms) /// on which Redde has been opened a minute or more before (the widget gallery is slow to /// find a new app's widgets). func testFunReady() throws { let app = try launch(["-echo.demoAlarm", "5", "-echo.demoBoard", "clear"]) XCTAssertTrue(app.navigationBars.buttons["New conversation"].firstMatch.waitForExistence(timeout: 20)) let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard") let allow = springboard.alerts.buttons["Allow"] if allow.waitForExistence(timeout: 8) { allow.tap() } hold(8) let stop = springboard.buttons["Stop"] if stop.waitForExistence(timeout: 6) { stop.tap() } XCUIDevice.shared.press(.home) hold(2) springboard.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.70)).press(forDuration: 1.6) XCTAssertTrue(springboard.buttons["Edit"].waitForExistence(timeout: 8), "no Edit") springboard.buttons["Edit"].tap() XCTAssertTrue(springboard.buttons["Add Widget"].waitForExistence(timeout: 8), "no Add Widget") springboard.buttons["Add Widget"].tap() let search = springboard.searchFields["Search Widgets"] XCTAssertTrue(search.waitForExistence(timeout: 8), "no search field") search.tap() search.typeText("Redde") let cell = springboard.cells.containing(NSPredicate(format: "label CONTAINS 'Redde'")).firstMatch XCTAssertTrue(cell.waitForExistence(timeout: 8), "no Redde in the gallery") cell.tap() hold(2) let wanted = springboard.staticTexts["From Your Agent"] for _ in 0 ..< 12 where !(wanted.exists && wanted.isHittable) { springboard.swipeLeft(); hold(1) } XCTAssertTrue(wanted.exists, "the agent's widget isn't in the gallery") springboard.swipeLeft() // its second size, the wide one hold(1) let add = springboard.buttons.matching(NSPredicate(format: "label CONTAINS 'Add Widget'")).firstMatch XCTAssertTrue(add.waitForExistence(timeout: 5), "no Add Widget button") add.tap() hold(1) if springboard.buttons["Done"].waitForExistence(timeout: 5) { springboard.buttons["Done"].tap() } hold(2) tidyHomeScreen(springboard) } /// The test runner's own icon into the Utilities folder, out of the Home Screen takes, and the /// Lock Screen cleared. private func tidyHomeScreen(_ springboard: XCUIApplication) { let runner = springboard.icons.matching(NSPredicate(format: "label BEGINSWITH 'EchoUITests'")).firstMatch let folder = springboard.icons["Utilities"] if runner.exists, runner.isHittable, folder.exists { runner.press(forDuration: 1.2, thenDragTo: folder, withVelocity: .slow, thenHoldForDuration: 1.5) hold(2) } XCUIDevice.shared.press(.home) hold(1) if springboard.buttons["Done"].exists { springboard.buttons["Done"].tap() } XCUIDevice.shared.press(.home) hold(1) // A new simulator's own notification ("Ready for Apple Intelligence") off the Lock Screen. XCUIDevice.shared.perform(NSSelectorFromString("pressLockButton")) hold(1) XCUIDevice.shared.perform(NSSelectorFromString("pressLockButton")) hold(2) let note = springboard.descendants(matching: .any).matching(NSPredicate(format: "label CONTAINS 'Apple Intelligence'")).firstMatch if note.waitForExistence(timeout: 4) { note.swipeLeft() hold(1) let clear = springboard.buttons.matching(NSPredicate(format: "label == 'Clear' OR label == 'Clear All'")).firstMatch if clear.waitForExistence(timeout: 3) { clear.tap() } } hold(1) XCUIDevice.shared.press(.home) hold(1) } func testFunTidy() throws { _ = try launch([]) XCUIDevice.shared.press(.home) hold(2) tidyHomeScreen(XCUIApplication(bundleIdentifier: "com.apple.springboard")) } /// The Phone tools store screenshot's two insets, as pictures in SHOT_DIR: the agent's widget /// on the Home Screen (as it was left: the `widget` take writes the shopping list on it) and a /// nine-minute timer counting down on the Lock Screen. On the simulator `testFunReady` /// readied, with no status bar override (one hides the countdown). func testToolsShotInsets() throws { let app = try launch(["-echo.demoAlarm", "540"]) let dir = URL(fileURLWithPath: try XCTUnwrap(ProcessInfo.processInfo.environment["SHOT_DIR"], "SHOT_DIR is where they go")) XCTAssertTrue(app.navigationBars.buttons["New conversation"].firstMatch.waitForExistence(timeout: 20)) hold(2) XCUIDevice.shared.press(.home) hold(3) try XCUIScreen.main.screenshot().pngRepresentation.write(to: dir.appending(path: "home-widget.png")) XCUIDevice.shared.perform(NSSelectorFromString("pressLockButton")) hold(1) XCUIDevice.shared.perform(NSSelectorFromString("pressLockButton")) hold(4) try XCUIScreen.main.screenshot().pngRepresentation.write(to: dir.appending(path: "lock-timer.png")) XCUIDevice.shared.press(.home) let clear = XCUIApplication() clear.launchArguments = XCUIApplication.straightToChat + ["-echo.demoAlarm", "clear"] clear.launch() hold(2) } /// Morning: the day from the calendar, then a reminder, which Phone tools asks about. func testFunMorning() throws { let app = try fun("morning") started(app) let allow = app.alerts.buttons["Allow"] XCTAssertTrue(allow.waitForExistence(timeout: 30), "Phone tools didn't ask about the reminder") mark("asked") hold(1.6) allow.tap() mark("allowed") XCTAssertTrue(says(app, "nudge you")) hold(2.5) mark("end") } /// A real timer, then the Lock Screen with it counting down. Cancelled again after. func testFunTimer() throws { let app = try fun("timer") started(app) XCTAssertTrue(says(app, "rings even")) hold(1.8) mark("locking") XCUIDevice.shared.perform(NSSelectorFromString("pressLockButton")) hold(1.0) XCUIDevice.shared.perform(NSSelectorFromString("pressLockButton")) mark("locked") hold(4.5) mark("end") XCUIDevice.shared.press(.home) let clear = XCUIApplication() clear.launchArguments = XCUIApplication.straightToChat + ["-echo.demoAlarm", "clear"] clear.launch() hold(2) } /// The list saved to Files and put on the widget, then the Home Screen with the widget. func testFunWidget() throws { let app = try fun("widget") started(app) XCTAssertTrue(says(app, "forget the parmesan")) hold(1.6) mark("home") XCUIDevice.shared.press(.home) hold(4.0) mark("end") } func testFunPhoto() throws { let app = try fun("photo") started(app) XCTAssertTrue(says(app, "Personal best")) hold(2.8) mark("end") } /// Voice mode with the camera on, then the question it went with and the answer. func testFunLook() throws { let app = try fun("look", ["-echo.voiceView", "-echo.voiceLook", "-echo.voiceDemo", "Is my plant okay? It looks kind of sad."]) let camera = app.descendants(matching: .any).matching(NSPredicate(format: "label == %@", "Camera")).firstMatch XCTAssertTrue(camera.waitForExistence(timeout: 30), "No camera button on the voice screen") hold(1.0) mark("start") hold(0.6) camera.tap() mark("camera") hold(2.8) // The voice screen's: the conversation beneath has a Stop of its own. let stop = app.buttons.matching(identifier: "Stop").allElementsBoundByIndex.last { $0.isHittable } XCTAssertNotNil(stop, "No Stop on the voice screen") stop?.tap() mark("closed") XCTAssertTrue(says(app, "perk up")) hold(2.5) mark("end") } func testFunReach() throws { let app = try fun("reach") started(app) XCTAssertTrue(says(app, "Tap Call")) mark("called") XCTAssertTrue(says(app, "Dana knows")) mark("sent") hold(2.5) mark("end") } /// Phone tools in Settings: each area with its own switch, and one of them being chosen. func testPhoneToolsSettings() throws { let app = try launch(["-echo.demoPhone", "settings", "-echo.screen", "settings", "-echo.demoPlugin", "there", "-theme", "claudeCode", "-appearance", "dark"]) XCTAssertTrue(app.navigationBars["Settings"].waitForExistence(timeout: 20)) app.openSettingsPage("Agent") let row = app.buttons.matching(NSPredicate(format: "label BEGINSWITH 'Phone tools'")).firstMatch for _ in 0 ..< 14 where !(row.exists && row.isHittable) { app.swipeUp() } hold(0.6) row.tap() XCTAssertTrue(app.navigationBars["Phone Tools"].waitForExistence(timeout: 10)) hold(1.2) mark("start") hold(2.2) app.buttons.matching(NSPredicate(format: "label BEGINSWITH 'Contacts'")).firstMatch.tap() hold(1.6) app.buttons["Allow reading"].tap() hold(1.8) mark("end") } /// Set up by code: Settings shows this connection as a QR code for another device to scan. func testSetUpByCode() throws { let app = try launch(["-echo.demo", "-echo.screen", "settings", "-theme", "claudeCode", "-appearance", "dark"]) XCTAssertTrue(app.navigationBars["Settings"].waitForExistence(timeout: 20)) let button = app.buttons["Set up another device…"].firstMatch for _ in 0 ..< 6 where !button.isHittable { app.swipeUp(velocity: .slow) } hold(1.5) mark("start") hold(0.8) button.tap() XCTAssertTrue(app.images["setup-code"].waitForExistence(timeout: 5)) hold(3) mark("end") } /// Make it yours: Settings with the app icons open, scrolled gently. func testMakeItYours() throws { let app = try launch(["-echo.demo", "-echo.screen", "settings", "-echo.expandAppIcons", "-theme", "standard", "-appearance", "light"]) XCTAssertTrue(app.navigationBars["Appearance"].waitForExistence(timeout: 20), "Settings → Appearance didn't open on the icons") hold(2) mark("start") hold(1.2) // On iPad Settings is a sheet in the middle of the screen: a drag from lower down starts outside it. let wide = app.windows.firstMatch.frame.width > 600 drag(app, from: CGVector(dx: 0.5, dy: wide ? 0.62 : 0.75), to: CGVector(dx: 0.5, dy: wide ? 0.42 : 0.5), velocity: 180) hold(2) mark("end") } // MARK: A day with Redde (design/video, `make_day.py`) /// The day's agents (Sol, Home, Coder) and chats, canned answers (`DemoDay`), in the Code /// theme, dark, as the other takes are. private func day(_ scene: String, _ extra: [String] = []) throws -> XCUIApplication { try launch(["-echo.demoProjects", "-echo.demoBots", "-echo.demoDay", scene, "-displayName", "Sol", "-echo.clearDrafts", "-sessions.botActionsShown", "NO", "-projectsInChatList", "NO", "-theme", "claudeCode", "-appearance", "dark"] + extra) } /// iOS asking to send notifications, answered (a fresh install's first launch of a scene that posts one). private func allowNotifications() { let allow = XCUIApplication(bundleIdentifier: "com.apple.springboard").alerts.buttons["Allow"] if allow.waitForExistence(timeout: 12) { allow.tap() } mark("allowed") } /// A slow drag from an element's middle, held at the end so what it shows there is seen. private func slide(_ element: XCUIElement, by dx: CGFloat, hold seconds: TimeInterval = 0.7) { let from = element.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)) from.press(forDuration: 0.08, thenDragTo: from.withOffset(CGVector(dx: dx, dy: 0)), withVelocity: 320, thenHoldForDuration: seconds) } /// A Form picker's menu, and one of its choices. private func pick(_ app: XCUIApplication, _ picker: String, _ choice: String) { let row = app.buttons.matching(NSPredicate(format: "label BEGINSWITH %@", picker)).firstMatch XCTAssertTrue(row.waitForExistence(timeout: 5), "no \(picker)") row.tap() hold(0.9) let item = app.buttons.matching(NSPredicate(format: "label == %@", choice)).firstMatch XCTAssertTrue(item.waitForExistence(timeout: 5), "no \(choice) in \(picker)") item.tap() mark(choice.lowercased().replacingOccurrences(of: " ", with: "-")) } /// The test runner's icon off the Home Screen (into the App Library), for the arrive-home take. func testDayTidy() throws { _ = try day("tidy") XCUIDevice.shared.press(.home) hold(1.5) let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard") let runner = springboard.icons.matching(NSPredicate(format: "label BEGINSWITH 'EchoUITests'")).firstMatch XCTAssertTrue(runner.waitForExistence(timeout: 5), "no runner icon") runner.press(forDuration: 1.4) let remove = springboard.buttons.matching(NSPredicate(format: "label CONTAINS 'Remove'")).firstMatch XCTAssertTrue(remove.waitForExistence(timeout: 5), "no Remove App") remove.tap() let fromHome = springboard.buttons.matching(NSPredicate(format: "label CONTAINS 'Home Screen'")).firstMatch XCTAssertTrue(fromHome.waitForExistence(timeout: 5), "no Remove from Home Screen") fromHome.tap() hold(1.5) XCUIDevice.shared.press(.home) hold(1) } /// A briefing's card on the deck at the top of the chat list (its words read as one element). private func deckCard(_ app: XCUIApplication, _ words: String) -> XCUIElement { app.descendants(matching: .any).matching(NSPredicate(format: "label CONTAINS %@", words)).firstMatch } /// 7:00: the chat list, the Week Ahead waiting in Briefings; the dentist's heads-up comes, as a /// banner and on top of the deck (its time, Leave by, the address); opened: the event card and /// the map. func testDayMorning() throws { let app = try day("morning", ["-echo.screen", "sessions", "-echo.demoDayAt", "3"]) allowNotifications() let card = deckCard(app, "Leave by") XCTAssertTrue(card.waitForExistence(timeout: 20), "the heads-up didn't come") mark("arrived") hold(3.2) card.tap() XCTAssertTrue(says(app, "Open in Calendar"), "no event card") mark("opened") XCTAssertTrue(app.buttons["Directions"].firstMatch.waitForExistence(timeout: 15), "no map") mark("map") hold(5.5) mark("end") } /// 7:02: the Week Ahead on top of the deck, its strip of days; opened: the week as a calendar, /// scrolled to its end. func testDayWeek() throws { let app = try day("week", ["-echo.screen", "sessions"]) let card = deckCard(app, "Week ahead") XCTAssertTrue(card.waitForExistence(timeout: 20), "no Week Ahead") hold(1.0) mark("start") hold(1.8) card.tap() XCTAssertTrue(says(app, "Team planning"), "no calendar") mark("opened") hold(2.2) drag(app, from: CGVector(dx: 0.5, dy: 0.8), to: CGVector(dx: 0.5, dy: 0.45), velocity: 260) mark("scrolled") hold(2.5) mark("end") } /// Idea cards: one put away with a swipe left, one taken with a swipe right, and Sol doing it. func testDayIdeas() throws { let app = try day("ideas", ["-echo.screen", "sessions"]) let first = app.staticTexts["Learn the bagpipes"].firstMatch XCTAssertTrue(first.waitForExistence(timeout: 20), "no idea cards") hold(1.0) mark("start") hold(1.2) slide(first, by: -230) mark("notnow") hold(1.4) let second = app.staticTexts["Book the vet for Pickles"].firstMatch XCTAssertTrue(second.waitForExistence(timeout: 5)) slide(second, by: 230) mark("doit") XCTAssertTrue(says(app, "tomorrow at 3:30")) mark("booked") hold(3.0) mark("end") } /// @Home in a chat with Sol: the bots offered, Home picked, the question sent, Home's answer /// here under its face. func testDayAside() throws { let app = try day("aside", ["-echo.fresh"]) let field = app.textFields.firstMatch XCTAssertTrue(field.waitForExistence(timeout: 20)) hold(1.5) mark("start") hold(0.8) field.tap() hold(0.5) field.typeText("@Ho") let home = app.buttons.containing(NSPredicate(format: "label CONTAINS %@", "@home")).firstMatch XCTAssertTrue(home.waitForExistence(timeout: 5), "Home wasn't offered") mark("offered") hold(1.2) home.tap() field.typeText("is the garage shut?") hold(0.6) app.buttons.matching(identifier: "Send").allElementsBoundByIndex.last { $0.isHittable }?.tap() mark("sent") XCTAssertTrue(says(app, "8:12")) mark("answered") hold(3.0) mark("end") } /// A question typed to Sol and sent; the reply's own words, once they show. private func ask(_ app: XCUIApplication, _ question: String) { let field = app.textFields.firstMatch XCTAssertTrue(field.waitForExistence(timeout: 20)) hold(1.5) mark("start") hold(0.6) field.tap() hold(0.4) field.typeText(question) hold(0.5) app.buttons.matching(identifier: "Send").allElementsBoundByIndex.last { $0.isHittable }?.tap() mark("sent") } /// 6:30 PM: "Where should we go for dinner?", Sol's choice card, one picked, and Sol on it, with /// a map of the place's address. func testDayDinner() throws { let app = try day("dinner", ["-echo.fresh"]) ask(app, "Where should we go for dinner?") let lupa = app.buttons.containing(NSPredicate(format: "label CONTAINS 'Lupa Trattoria'")).firstMatch XCTAssertTrue(lupa.waitForExistence(timeout: 15), "no choice card") mark("card") XCTAssertTrue(says(app, "all open now")) hold(2.0) lupa.tap() mark("picked") XCTAssertTrue(says(app, "Table for four")) mark("confirmed") // Its address gets a map (`AddressMaps`), under the reply: brought up over the composer. XCTAssertTrue(app.buttons["Directions"].firstMatch.waitForExistence(timeout: 15), "no map") mark("map") hold(1.2) drag(app, from: CGVector(dx: 0.5, dy: 0.72), to: CGVector(dx: 0.5, dy: 0.52), velocity: 220) mark("scrolled") hold(3.5) mark("end") } /// 8:00 PM: the cabin's packing list as a checklist card, the last three things ticked off. func testDayPacking() throws { let app = try day("packing", ["-echo.fresh"]) ask(app, "Make me a packing list for the cabin") XCTAssertTrue(says(app, "Cabin weekend"), "no checklist card") mark("card") XCTAssertTrue(says(app, "packed already")) hold(1.6) for item in ["Rain jackets", "S'mores supplies", "Fishing rods"] { // (a toggle since the 10-30 redesign, not a button) let row = app.descendants(matching: .any).matching(NSPredicate(format: "label CONTAINS %@", item)).firstMatch XCTAssertTrue(row.waitForExistence(timeout: 5), "no \(item)") row.tap() mark("ticked") hold(0.9) } mark("done") hold(3.0) mark("end") } // MARK: The 2.3 day (design/video, `make_day23.py`): the new cards and proactive moments /// 7:30: "What's my day look like?", today as a timeline (the walk over, a free stretch, the /// now line); the dentist tapped open in place. func testDayToday() throws { let app = try day("today", ["-echo.fresh"]) ask(app, "What's my day look like?") XCTAssertTrue(says(app, "Free for"), "no timeline") mark("card") XCTAssertTrue(says(app, "when to leave")) mark("answered") hold(2.6) let dentist = app.buttons.containing(NSPredicate(format: "label CONTAINS 'Dentist'")).firstMatch if dentist.waitForExistence(timeout: 5) { dentist.tap() } mark("opened") hold(3.0) mark("end") } /// 10:15: "Good coffee near me?", answered from where the phone is: the places on a map, /// walks measured from here. func testDayCoffee() throws { let app = try day("coffee", ["-echo.fresh"]) ask(app, "Good coffee near me?") XCTAssertTrue(app.buttons.containing(NSPredicate(format: "label CONTAINS 'Directions to'")).firstMatch.waitForExistence(timeout: 30), "no places card") mark("card") hold(5.0) mark("end") } /// 12:10: "Will it rain this afternoon?", the weather card with the storms called out. func testDayRain() throws { let app = try day("rain", ["-echo.fresh"]) ask(app, "Will it rain this afternoon?") XCTAssertTrue(says(app, "Storms likely"), "no weather card") mark("card") hold(5.0) mark("end") } /// 1:30 PM: the dentist's heads-up arriving (banner, deck), opened: the event card and the map. func testDayAfternoon() throws { let app = try day("afternoon", ["-echo.screen", "sessions", "-echo.demoDayAt", "3"]) allowNotifications() let card = deckCard(app, "Leave by") XCTAssertTrue(card.waitForExistence(timeout: 20), "the heads-up didn't come") mark("arrived") hold(3.2) card.tap() XCTAssertTrue(says(app, "Open in Calendar"), "no event card") mark("opened") XCTAssertTrue(app.buttons["Directions"].firstMatch.waitForExistence(timeout: 15), "no map") mark("map") hold(5.5) mark("end") } /// 2:00 PM: the Leave-By Alert, worked out on the phone, as a banner over the chat list; then /// held open, with Open in Maps. func testDayLeave() throws { _ = try day("leave", ["-echo.screen", "sessions", "-echo.demoDayAt", "3"]) allowNotifications() let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard") let banner = springboard.descendants(matching: .any).matching(NSPredicate(format: "label CONTAINS 'Time to leave'")).firstMatch XCTAssertTrue(banner.waitForExistence(timeout: 20), "no Leave-By Alert") mark("banner") hold(3.0) if ProcessInfo.processInfo.environment["PROMO_EXPAND"] == "1" { banner.press(forDuration: 1.0) mark("expanded") hold(3.5) } mark("end") } /// 7:50, again: the timeline's free stretch tapped, Apple's new-event editor at its start, a /// title typed, Add. func testDayAddEvent() throws { let app = try day("today", ["-echo.fresh"]) ask(app, "What's my day look like?") let free = app.buttons.containing(NSPredicate(format: "label CONTAINS 'Free for'")).firstMatch XCTAssertTrue(free.waitForExistence(timeout: 30), "no free stretch") XCTAssertTrue(says(app, "when to leave")) mark("card") hold(2.0) free.tap() mark("tapped") let title = app.textFields["Title"].firstMatch XCTAssertTrue(title.waitForExistence(timeout: 10), "no event editor") mark("editor") hold(1.2) title.tap() title.typeText("Walk by the lake") mark("typed") hold(1.0) // Add: the editor's tick, top right (Apple's editor; its button isn't found by name here). app.windows.firstMatch.coordinate(withNormalizedOffset: CGVector(dx: 0.89, dy: 0.105)).tap() mark("added") hold(2.5) mark("end") } /// 10:40: a watch and a follow-up asked for in words. func testDayWatchAsk() throws { let app = try day("watchask", ["-echo.fresh"]) ask(app, "Let me know when the bike part ships. I'll call the plumber tomorrow.") XCTAssertTrue(says(app, "plumber tomorrow at 9"), "no answer") mark("answered") hold(3.5) mark("end") } /// 1:45 PM: the traffic re-check, heavier: leave by 1:55 now. func testDayTraffic() throws { _ = try day("traffic", ["-echo.screen", "sessions", "-echo.demoDayAt", "3"]) allowNotifications() let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard") let banner = springboard.descendants(matching: .any).matching(NSPredicate(format: "label CONTAINS 'traffic is heavier'")).firstMatch XCTAssertTrue(banner.waitForExistence(timeout: 20), "no re-check") mark("banner") hold(4.5) mark("end") } /// 4:20 PM: the bike part's watch says it shipped, as a banner over Watches & Follow-ups, and /// its row says so too, with the plumber's follow-up below. func testDayShipped() throws { let app = try day("shipped", ["-echo.screen", "watches", "-echo.demoDayAt", "4"]) allowNotifications() XCTAssertTrue(app.navigationBars["Watches & Follow-ups"].waitForExistence(timeout: 20), "no Watches") mark("start") let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard") let banner = springboard.descendants(matching: .any).matching(NSPredicate(format: "label CONTAINS 'bike part'")).firstMatch XCTAssertTrue(banner.waitForExistence(timeout: 20), "no watch note") mark("banner") XCTAssertTrue(says(app, "Arriving Thursday"), "the row didn't change") mark("row") hold(3.5) mark("end") } /// 11:30: Coder's approval card in plain words (the delete in red); Allow Once. func testDayApproval() throws { let app = try day("approval") let allow = app.buttons["Allow Once"].firstMatch XCTAssertTrue(allow.waitForExistence(timeout: 20), "no approval card") hold(1.0) mark("start") hold(3.0) allow.tap() mark("allowed") hold(3.0) mark("end") } /// 3:35 PM: "Tell Ana I'm running 15 minutes late for pickup", the email drawn as itself; Send. func testDayEmail() throws { let app = try day("email", ["-echo.fresh"]) ask(app, "Tell Ana I'm running 15 minutes late for pickup") XCTAssertTrue(says(app, "Running late for pickup"), "no email card") mark("card") XCTAssertTrue(says(app, "tap Send")) hold(2.8) let send = app.buttons.matching(NSPredicate(format: "label == 'Send'")).allElementsBoundByIndex.last { $0.isHittable } XCTAssertNotNil(send, "no Send on the card") send?.tap() mark("sent") XCTAssertTrue(says(app, "writes back")) mark("answered") hold(3.0) mark("end") } /// Triggers from Your iPhone, down to its ideas (arriving home first among them). func testDayTriggers() throws { let app = try day("triggers", ["-echo.screen", "triggers"]) XCTAssertTrue(app.navigationBars["Triggers from Your iPhone"].waitForExistence(timeout: 20)) hold(1.0) mark("start") hold(1.0) drag(app, from: CGVector(dx: 0.5, dy: 0.78), to: CGVector(dx: 0.5, dy: 0.42), velocity: 260) mark("scrolled") hold(3.0) mark("end") } /// Arriving home: Redde in the background, as an automation runs it, and Home's answer comes /// as Redde's notification over the Home Screen. func testDayHome() throws { let app = try day("home", ["-echo.demoDayAt", "7"]) allowNotifications() XCTAssertTrue(app.textFields.firstMatch.waitForExistence(timeout: 20)) hold(0.8) XCUIDevice.shared.press(.home) mark("home") let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard") let banner = springboard.descendants(matching: .any).matching(NSPredicate(format: "label CONTAINS 'Welcome home'")).firstMatch XCTAssertTrue(banner.waitForExistence(timeout: 20), "no notification") mark("banner") hold(4.0) mark("end") } /// 21:00: the Evening wrap-up on top of the Briefings deck, opened. func testDayEvening() throws { let app = try day("evening", ["-echo.screen", "sessions"]) let row = deckCard(app, "Evening wrap-up") XCTAssertTrue(row.waitForExistence(timeout: 20), "no wrap-up") hold(1.0) mark("start") hold(1.8) row.tap() XCTAssertTrue(says(app, "Waiting on you")) mark("opened") hold(3.5) mark("end") } /// Make This a Routine over the dinner plan: every week, on Sundays. func testDayRoutine() throws { let app = try day("routine") XCTAssertTrue(app.navigationBars["Make This a Routine"].waitForExistence(timeout: 20)) hold(0.8) mark("start") hold(0.8) pick(app, "Run It", "Every week") hold(0.8) pick(app, "Day", "Sunday") hold(2.0) mark("end") } /// Agent Activity, the day so far. func testDayActivity() throws { let app = try day("activity", ["-echo.screen", "activity"]) XCTAssertTrue(app.navigationBars["Agent Activity"].waitForExistence(timeout: 20)) XCTAssertTrue(says(app, "nothing new")) hold(0.8) mark("start") hold(3.5) mark("end") } /// Plugin Updates: three, all updated at once. func testDayPlugins() throws { let app = try day("plugins", ["-echo.screen", "pluginUpdates"]) let all = app.buttons["Update All"].firstMatch XCTAssertTrue(all.waitForExistence(timeout: 20)) hold(0.8) mark("start") hold(1.0) all.tap() mark("updating") hold(3.2) mark("end") } /// New Watch on the garage door: how often, Home watching, a change handed to Sol. func testDayWatch() throws { let app = try day("watch") XCTAssertTrue(app.navigationBars["New Watch"].waitForExistence(timeout: 20)) hold(0.8) mark("start") hold(0.6) pick(app, "How often", "Every 15 minutes") hold(0.6) pick(app, "Who Watches", "Home") hold(0.6) pick(app, "When It Changes", "Hand It to Sol") hold(2.2) mark("end") } // MARK: Redde 2.4 (design/video, `make_24.py`) /// 2.4's sample data in the standard theme, dark, as the 2.4 screenshots: the inbox, two goals, /// the briefings, the bots and the made-up Coming Up day, on the chat list. private func v24(_ extra: [String] = []) throws -> XCUIApplication { try launch(["-echo.demoInbox", "two", "-echo.demoGoals", "-echo.demoBriefings", "-echo.demoComingUp", "-echo.demoProjects", "-echo.demoBots", "-displayName", "Sol", "-echo.clearDrafts", "-theme", "standard", "-appearance", "dark"] + extra) } /// The goal's tile in the Today row tapped: its page, then down to the plan. func testV24Goal() throws { let app = try v24(["-echo.screen", "sessions"]) let tile = app.staticTexts["Run a 10K by March"].firstMatch XCTAssertTrue(tile.waitForExistence(timeout: 20), "no goal tile") hold(1.5) mark("start") hold(1.0) tile.tap() XCTAssertTrue(app.staticTexts["Last Check-in"].firstMatch.waitForExistence(timeout: 10)) mark("open") hold(2.5) drag(app, from: CGVector(dx: 0.5, dy: 0.8), to: CGVector(dx: 0.5, dy: 0.35)) mark("plan") hold(2.5) mark("end") } /// The Inbox's tile tapped: Important, then Needs Reply, then an email in the reader. func testV24Inbox() throws { let app = try v24(["-echo.screen", "sessions"]) let tile = app.staticTexts["Inbox"].firstMatch XCTAssertTrue(tile.waitForExistence(timeout: 20), "no inbox tile") hold(1.5) mark("start") hold(1.0) tile.tap() let reply = app.buttons.containing(NSPredicate(format: "label BEGINSWITH %@", "Needs Reply")).firstMatch XCTAssertTrue(reply.waitForExistence(timeout: 10), "no piles") mark("open") hold(2.0) reply.tap() mark("reply") hold(2.0) let row = app.cells.firstMatch XCTAssertTrue(row.waitForExistence(timeout: 5), "no email") row.tap() mark("email") hold(3.0) mark("end") } /// The Today row slid along, then Planned: what the agent will do next. func testV24Today() throws { let app = try v24(["-echo.screen", "sessions"]) let tile = app.staticTexts["Inbox"].firstMatch XCTAssertTrue(tile.waitForExistence(timeout: 20), "no Today row") hold(1.5) mark("start") hold(1.2) slide(tile, by: -320, hold: 0.4) mark("slid") hold(1.6) // (the segmented tab: a hidden ⌘2 button is also called Planned) app.segmentedControls.buttons["Planned"].firstMatch.tap() mark("planned") hold(3.5) mark("end") } /// Memory: a fact added in words and saved. func testV24Memory() throws { // (only the memory's sample data: with the rest, something presented at launch closes the // page again a few seconds in) let app = try launch(["-echo.demoMemory", "-echo.screen", "memory", "-echo.clearDrafts", "-theme", "standard", "-appearance", "dark"]) let add = app.navigationBars["Memory"].waitForExistence(timeout: 20) ? app.collectionViews.buttons["Add"].firstMatch : app.buttons["no memory page"] XCTAssertTrue(add.waitForExistence(timeout: 20), "no memory page") hold(1.2) mark("start") hold(1.0) add.tap() hold(0.6) app.typeText("Training for a 10K in March.") // (the new row has the focus) mark("typed") hold(1.0) let save = app.navigationBars.buttons["Save"].firstMatch XCTAssertTrue(save.waitForExistence(timeout: 5), "no Save") save.tap() mark("saved") hold(2.5) mark("end") } }