Ctrl K
ComunidadGuías…

EchoUITests/LayoutChangeUITests.swift

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import XCTest
import notify

/// Opening or closing an iPhone Duo swaps the iPhone's layout for the wide one, or back, while
/// Redde runs, and the chat screen is built again in the other (`LayoutCarry`). `-echo.flipLayout`
/// swaps it when the Darwin notification is posted: here, at the moment each test wants.
@MainActor
final class LayoutChangeUITests: XCTestCase {
    private let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")

    private func launch(_ extra: [String]) -> XCUIApplication {
        continueAfterFailure = false
        let app = XCUIApplication()
        app.launchArguments = ["-echo.flipLayout", "-echo.clearDrafts"] + extra + XCUIApplication.chatsSection + XCUIApplication.straightToChat
        app.launch()
        return app
    }

    /// The button of that name that is on screen: the iPhone's conversation list stays built
    /// while its panel is closed, with a More menu of its own.
    private func tapOnScreen(_ name: String, in place: XCUIElementTypeQueryProvider) {
        let named = place.buttons.matching(NSPredicate(format: "label == %@ OR identifier == %@", name, name))
        XCTAssertTrue(named.firstMatch.waitForExistence(timeout: 5), "No \(name) button")
        var button: XCUIElement?
        for _ in 0 ..< 10 where button == nil {
            button = named.allElementsBoundByIndex.first { $0.isHittable }
            if button == nil { Thread.sleep(forTimeInterval: 0.3) }
        }
        XCTAssertNotNil(button, "No \(name) button on screen")
        button?.tap()
    }

    /// The chat's More menu: not a reply's (its action row has a More too), and wherever the bar
    /// is (down the side of an iPhone Duo's outer display, in the iOS 27.1 SDK).
    private func openChatMenu(_ app: XCUIApplication) {
        let inReplies = Set(app.scrollViews.buttons.matching(identifier: "More").allElementsBoundByIndex.map { "\($0.frame)" })
        let menu = app.buttons.matching(identifier: "More").allElementsBoundByIndex.first { $0.isHittable && !inReplies.contains("\($0.frame)") }
        XCTAssertNotNil(menu, "No More menu on the chat")
        menu?.tap()
    }

    /// The layout swapped, and a moment for the other to be built and settle.
    private func flip() {
        notify_post("com.goosehouse.echo.debug.flipLayout")
        Thread.sleep(forTimeInterval: 1.5)
    }

    /// The longer texts in the middle of the screen (between the header and the composer), read in
    /// one snapshot.
    private func textsOnScreen(_ app: XCUIApplication) throws -> Set<String> {
        let window = app.windows.firstMatch.frame
        var found: Set<String> = []
        func walk(_ element: XCUIElementSnapshot) {
            if element.elementType == .staticText, element.label.count > 12,
               element.frame.minY > window.minY + 100, element.frame.maxY < window.maxY - 120 {
                found.insert(element.label)
            }
            element.children.forEach(walk)
        }
        walk(try app.scrollViews.firstMatch.snapshot())
        return found
    }

    /// Read back up a long conversation: through the change and back, the same words are on screen,
    /// not the end.
    func testTheReaderKeepsTheirPlace() throws {
        let app = launch(["-echo.demoLong"])
        let end = app.staticTexts["End of the long demo."]
        XCTAssertTrue(end.waitForExistence(timeout: 15), "The long demo didn't open at its end")
        let transcript = app.scrollViews.firstMatch
        transcript.swipeDown(velocity: .fast)
        transcript.swipeDown(velocity: .fast)
        Thread.sleep(forTimeInterval: 1.5)
        XCTAssertFalse(end.isHittable, "Not scrolled back")
        let before = try textsOnScreen(app)
        XCTAssertFalse(before.isEmpty, "Nothing read on screen")
        for side in ["the other layout", "back"] {
            flip()
            XCTAssertFalse(end.isHittable, "In \(side), the transcript went back to its end")
            let now = try textsOnScreen(app)
            XCTAssertFalse(now.isDisjoint(with: before), "In \(side), none of \(before.prefix(3)) is on screen any more, but \(now.prefix(3))")
        }
    }

    /// Half a message typed: it is still there, with the keyboard up, to go on typing.
    func testTypingCarriesOn() {
        let app = launch(["-echo.demo", "-echo.demoShort"])
        let field = app.textFields.firstMatch
        XCTAssertTrue(field.waitForExistence(timeout: 20), "No message field")
        field.tap()
        field.typeText("half a thought")
        for side in ["the other layout", "back"] {
            flip()
            let now = app.textFields.firstMatch
            XCTAssertEqual(now.value as? String, "half a thought", "In \(side), the draft went")
            XCTAssertTrue(app.keyboards.firstMatch.waitForExistence(timeout: 3), "In \(side), the keyboard went")
            now.typeText(" more")
            XCTAssertEqual(now.value as? String, "half a thought more", "In \(side), typing doesn't go on in the field")
            now.typeText(String(repeating: XCUIKeyboardKey.delete.rawValue, count: 5))
        }
    }

    /// Voice mode stays up through the change: the conversation goes on.
    func testVoiceModeStaysUp() {
        let app = launch(["-echo.demo", "-echo.demoShort", "-handsFreeByDefault", "NO"])
        let talk = app.buttons["Talk"]
        XCTAssertTrue(talk.waitForExistence(timeout: 20), "No Talk button beside the message field")
        talk.tap()
        let typing = app.buttons["Switch to typing"]
        for _ in 0 ..< 3 where !typing.waitForExistence(timeout: 8) {
            for label in ["Allow", "OK"] where springboard.alerts.buttons[label].exists { springboard.alerts.buttons[label].tap() }
        }
        XCTAssertTrue(typing.exists, "The voice screen didn't open")
        for side in ["the other layout", "back"] {
            flip()
            XCTAssertTrue(typing.exists, "In \(side), the voice screen closed")
        }
    }

    /// A message half written to a group chat goes over with it.
    func testAGroupChatsDraftCarriesOn() {
        let app = launch(["-echo.demoProjects", "-echo.demoBots", "-echo.openGroup", "-displayName", "Redde"])
        // `-echo.openGroup` opens the group chat from the conversation list, which the wide layout
        // has on screen: there first.
        XCTAssertTrue(app.textFields.firstMatch.waitForExistence(timeout: 20), "Redde didn't open")
        flip()
        func field() -> XCUIElement { app.textFields["group-composer"] }
        XCTAssertTrue(field().waitForExistence(timeout: 20), "No message field in the group chat")
        field().tap()
        field().typeText("for all of you")
        XCTAssertEqual(field().value as? String, "for all of you", "Typing didn't reach the group chat's field")
        for side in ["the other layout", "back"] {
            flip()
            XCTAssertEqual(field().value as? String, "for all of you", "In \(side), the group chat's draft went")
        }
    }

    /// Settings, opened from the chat's menu, stays open: it is the app's, not the layout's.
    func testSettingsStaysOpen() {
        let app = launch(["-echo.demo", "-echo.demoShort"])
        XCTAssertTrue(app.buttons["More"].firstMatch.waitForExistence(timeout: 20), "No More menu on the chat")
        openChatMenu(app)
        tapOnScreen("Settings", in: app)
        let settings = app.navigationBars["Settings"]
        XCTAssertTrue(settings.waitForExistence(timeout: 5), "Settings didn't open")
        for side in ["the other layout", "back"] {
            flip()
            XCTAssertTrue(settings.exists, "In \(side), Settings closed")
        }
    }

    /// Find in conversation keeps its words and stays open.
    func testFindKeepsItsWords() {
        let app = launch(["-echo.demoLong"])
        XCTAssertTrue(app.staticTexts["End of the long demo."].waitForExistence(timeout: 15), "The long demo didn't open")
        openChatMenu(app)
        tapOnScreen("Find in conversation", in: app)
        let find = app.textFields["Find in conversation"]
        XCTAssertTrue(find.waitForExistence(timeout: 5), "Find didn't open")
        find.typeText("kitchen")
        for side in ["the other layout", "back"] {
            flip()
            XCTAssertEqual(app.textFields["Find in conversation"].value as? String, "kitchen", "In \(side), Find lost its words")
        }
    }
}