import XCTest /// Board list → lanes → cards → card detail, then an edit that has to reach disk — the one /// end-to-end walk of the mobile MVP's navigation stack (`BoardRoute`'s three destinations), plus /// the transactional edit sheet `CardDetailScreen`'s Edit button presents. final class BoardsNavigationUITests: XCTestCase { @MainActor func testBoardsNavigationAndTitleEdit() throws { let (app, root) = XCUIApplication.launchedWithFixtureBoard() app.navigateToFirstCard() // CardDetailScreen is read-only: the fixture card's title renders as plain text, not a // field — the read/edit split this suite now has to cross to reach any field at all. XCTAssertTrue( app.staticTexts[RichBoard.firstLaneFirstCard].waitForExistence(timeout: XCUIApplication.uiTimeout), "the read view's title text never appeared" ) app.openCardEdit() // CardEditScreen opens on the Details pane; its only textField is the title // (`Section("Title")`) — the body pane is a bare `TextEditor`, a `textView`, and is not // this pane's concern. let titleField = app.textFields.firstMatch XCTAssertTrue( titleField.waitForExistence(timeout: XCUIApplication.uiTimeout), "CardEditScreen's title field never appeared" ) XCTAssertEqual( titleField.value as? String, RichBoard.firstLaneFirstCard, "the title field did not seed with the fixture card's own title" ) let sentinel = "Retitled by UI test" titleField.replaceAllText(with: sentinel) app.navigationBars.buttons["Save"].tap() // The cover dismisses back to the read view underneath, which should already show the new // title — `session.perform` awaits its own reload before `Save` dismisses. XCTAssertTrue( app.staticTexts[sentinel].waitForExistence(timeout: XCUIApplication.uiTimeout), "the read view never picked up the retitled card" ) XCTAssertTrue( waitForFile(under: root, containing: sentinel), "the retitled card never landed on disk under \(root.path)" ) } @MainActor func testCardEditBodyPaneWritesToDisk() throws { let (app, root) = XCUIApplication.launchedWithFixtureBoard() app.navigateToFirstCard() app.openCardEdit() app.buttons["Body"].tap() let bodyEditor = app.textViews.firstMatch XCTAssertTrue( bodyEditor.waitForExistence(timeout: XCUIApplication.uiTimeout), "the Body pane's text editor never appeared" ) let sentinel = "Rewritten body by UI test" bodyEditor.replaceAllText(with: sentinel) app.navigationBars.buttons["Save"].tap() // The read view renders the body as Markdown; a plain sentinel with no markup renders // back out as itself. XCTAssertTrue( app.staticTexts[sentinel].waitForExistence(timeout: XCUIApplication.uiTimeout), "the read view never picked up the rewritten body" ) XCTAssertTrue( waitForFile(under: root, containing: sentinel), "the rewritten body never landed on disk under \(root.path)" ) } @MainActor func testCardEditCancelWithDirtyDraftsDiscardsOnDisk() throws { let (app, root) = XCUIApplication.launchedWithFixtureBoard() app.navigateToFirstCard() app.openCardEdit() let titleField = app.textFields.firstMatch XCTAssertTrue( titleField.waitForExistence(timeout: XCUIApplication.uiTimeout), "CardEditScreen's title field never appeared" ) let sentinel = "Abandoned edit" titleField.replaceAllText(with: sentinel) // Cancel on a dirty sheet raises a confirmation rather than dismissing outright — the // sheet's only exit, since a `fullScreenCover` has no interactive swipe-dismiss. app.navigationBars.buttons["Cancel"].tap() let discardButton = app.buttons["Discard"] XCTAssertTrue( discardButton.waitForExistence(timeout: XCUIApplication.uiTimeout), "the discard confirmation never appeared for a dirty Cancel" ) discardButton.tap() // Back on the read view, still showing the fixture's own title — nothing was ever written. XCTAssertTrue( app.staticTexts[RichBoard.firstLaneFirstCard].waitForExistence(timeout: XCUIApplication.uiTimeout), "the read view did not return to the card's original title after Discard" ) XCTAssertTrue( fileDoesNotContain(under: root, substring: sentinel), "the discarded title leaked onto disk under \(root.path)" ) } } private extension XCUIApplication { // `navigateToFirstCard()` moved to `MobileUITestSupport.swift` — the comments suite starts on // the same card. /// Taps `CardDetailScreen`'s Edit button and waits for `CardEditScreen`'s cover to land. @MainActor func openCardEdit() { let editButton = navigationBars.buttons["Edit"] XCTAssertTrue( editButton.waitForExistence(timeout: XCUIApplication.uiTimeout), "the read view's Edit button never appeared" ) editButton.tap() XCTAssertTrue( navigationBars.buttons["Save"].waitForExistence(timeout: XCUIApplication.uiTimeout), "CardEditScreen's cover never appeared" ) } }