The comment thread renders read-only under the card's body (author line, Markdown body through CardBodyView, read-only paperclip rows), read outside the snapshot and re-read on every walk landing via BoardSession.snapshotGeneration. Add Comment posts through the Mac composer's own draft-then-rename bracket — seeding from the card's single synced draft so a thought started on the Mac finishes here — and each row's context menu opens the same sheet in edit mode. Both commit on their trailing button or not at all: the phone's transactional model, dirty-Cancel confirmation and swipe-dismiss disabled while dirty included. UI-tested end to end with disk assertions. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
142 lines
5.5 KiB
Swift
142 lines
5.5 KiB
Swift
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"
|
|
)
|
|
}
|
|
}
|