The phone reads the thread — comments arrive on the card's read view, posting and editing behind transactional sheets
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
This commit is contained in:
@@ -120,32 +120,8 @@ final class BoardsNavigationUITests: XCTestCase {
|
||||
|
||||
private extension XCUIApplication {
|
||||
|
||||
/// Boards → the fixture's first lane → its first card, landing on `CardDetailScreen`'s read
|
||||
/// view. Shared by every test in this file so the navigation-and-assert boilerplate is written
|
||||
/// once.
|
||||
@MainActor
|
||||
func navigateToFirstCard() {
|
||||
let boardRow = element(labelContaining: RichBoard.title)
|
||||
XCTAssertTrue(
|
||||
boardRow.waitForExistence(timeout: XCUIApplication.uiTimeout),
|
||||
"the \"\(RichBoard.title)\" row never appeared — check the fixture copy or the first scan"
|
||||
)
|
||||
boardRow.tap()
|
||||
|
||||
let laneRow = element(labelContaining: RichBoard.firstLane)
|
||||
XCTAssertTrue(
|
||||
laneRow.waitForExistence(timeout: XCUIApplication.uiTimeout),
|
||||
"the \"\(RichBoard.firstLane)\" lane row never appeared"
|
||||
)
|
||||
laneRow.tap()
|
||||
|
||||
let cardRow = element(labelContaining: RichBoard.firstLaneFirstCard)
|
||||
XCTAssertTrue(
|
||||
cardRow.waitForExistence(timeout: XCUIApplication.uiTimeout),
|
||||
"the \"\(RichBoard.firstLaneFirstCard)\" card row never appeared"
|
||||
)
|
||||
cardRow.tap()
|
||||
}
|
||||
// `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
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import XCTest
|
||||
|
||||
/// The read screen's comment thread, end to end: the fixture card's own posted comment renders
|
||||
/// read-only, the composer posts a new comment through the draft-then-rename bracket, and the
|
||||
/// context menu's Edit rewrites a posted body — each write asserted on disk, not just on screen.
|
||||
final class CardCommentsUITests: XCTestCase {
|
||||
|
||||
@MainActor
|
||||
func testCommentsReadAddAndEdit() throws {
|
||||
let (app, root) = XCUIApplication.launchedWithFixtureBoard()
|
||||
app.navigateToFirstCard()
|
||||
|
||||
// Read mode first: the fixture card carries exactly one posted comment (author "rzen",
|
||||
// dated, never edited), so the header's count and the author line are both assertable
|
||||
// before this test writes anything.
|
||||
let header = app.element(labelContaining: "Comments · 1")
|
||||
XCTAssertTrue(
|
||||
header.waitForExistence(timeout: XCUIApplication.uiTimeout),
|
||||
"the comments header never appeared with the fixture's own count"
|
||||
)
|
||||
XCTAssertTrue(
|
||||
app.element(labelContaining: "rzen").exists,
|
||||
"the fixture comment's author line never rendered"
|
||||
)
|
||||
|
||||
// Post a new comment. The Add button sits at the very bottom of the scroll view, below
|
||||
// the fixture card's body and footer.
|
||||
let addButton = app.buttons["Add Comment"]
|
||||
app.scrollUntilHittable(addButton)
|
||||
addButton.tap()
|
||||
|
||||
let postButton = app.navigationBars.buttons["Post"]
|
||||
XCTAssertTrue(
|
||||
postButton.waitForExistence(timeout: XCUIApplication.uiTimeout),
|
||||
"the composer sheet never appeared"
|
||||
)
|
||||
|
||||
let composer = app.textViews.firstMatch
|
||||
XCTAssertTrue(
|
||||
composer.waitForExistence(timeout: XCUIApplication.uiTimeout),
|
||||
"the composer's text editor never appeared"
|
||||
)
|
||||
let sentinel = "Posted from the phone by UI test"
|
||||
composer.tap()
|
||||
composer.typeText(sentinel)
|
||||
postButton.tap()
|
||||
|
||||
// The sheet dismisses only on the perform's success, and the perform awaits its own
|
||||
// reload — so by the time the thread re-reads, both the screen and the disk carry it.
|
||||
XCTAssertTrue(
|
||||
app.element(labelContaining: "Comments · 2").waitForExistence(timeout: XCUIApplication.uiTimeout),
|
||||
"the header never counted the posted comment"
|
||||
)
|
||||
XCTAssertTrue(
|
||||
app.staticTexts[sentinel].waitForExistence(timeout: XCUIApplication.uiTimeout),
|
||||
"the posted comment's body never rendered in the thread"
|
||||
)
|
||||
XCTAssertTrue(
|
||||
waitForFile(under: root, containing: sentinel),
|
||||
"the posted comment never landed on disk under \(root.path)"
|
||||
)
|
||||
|
||||
// Edit the comment just posted, through its row's context menu.
|
||||
let postedBody = app.staticTexts[sentinel]
|
||||
postedBody.press(forDuration: 1.0)
|
||||
|
||||
let editAction = app.buttons["Edit Comment"]
|
||||
XCTAssertTrue(
|
||||
editAction.waitForExistence(timeout: XCUIApplication.uiTimeout),
|
||||
"the context menu's Edit Comment action never appeared"
|
||||
)
|
||||
editAction.tap()
|
||||
|
||||
let saveButton = app.navigationBars.buttons["Save"]
|
||||
XCTAssertTrue(
|
||||
saveButton.waitForExistence(timeout: XCUIApplication.uiTimeout),
|
||||
"the edit sheet never appeared"
|
||||
)
|
||||
let editor = app.textViews.firstMatch
|
||||
XCTAssertEqual(
|
||||
editor.value as? String, sentinel,
|
||||
"the edit sheet did not seed with the posted comment's body"
|
||||
)
|
||||
|
||||
let rewritten = "Rewritten from the phone by UI test"
|
||||
editor.replaceAllText(with: rewritten)
|
||||
saveButton.tap()
|
||||
|
||||
XCTAssertTrue(
|
||||
app.staticTexts[rewritten].waitForExistence(timeout: XCUIApplication.uiTimeout),
|
||||
"the rewritten comment never rendered in the thread"
|
||||
)
|
||||
XCTAssertTrue(
|
||||
waitForFile(under: root, containing: rewritten),
|
||||
"the rewritten comment never landed on disk under \(root.path)"
|
||||
)
|
||||
XCTAssertTrue(
|
||||
fileDoesNotContain(under: root, substring: sentinel),
|
||||
"the pre-edit body survived on disk — the edit did not replace the body span"
|
||||
)
|
||||
// The edited indicator is `modified` differing from `created` — the save just made that
|
||||
// true, and the author line renders it as a trailing "· edited".
|
||||
XCTAssertTrue(
|
||||
app.element(labelContaining: "· edited").waitForExistence(timeout: XCUIApplication.uiTimeout),
|
||||
"the edited marker never appeared on the rewritten comment's author line"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -194,6 +194,32 @@ extension XCUIApplication {
|
||||
confirm.tap()
|
||||
}
|
||||
|
||||
/// Boards → the fixture's first lane → its first card, landing on `CardDetailScreen`'s read
|
||||
/// view. Shared navigation boilerplate for every test that starts on a card.
|
||||
@MainActor
|
||||
func navigateToFirstCard() {
|
||||
let boardRow = element(labelContaining: RichBoard.title)
|
||||
XCTAssertTrue(
|
||||
boardRow.waitForExistence(timeout: Self.uiTimeout),
|
||||
"the \"\(RichBoard.title)\" row never appeared — check the fixture copy or the first scan"
|
||||
)
|
||||
boardRow.tap()
|
||||
|
||||
let laneRow = element(labelContaining: RichBoard.firstLane)
|
||||
XCTAssertTrue(
|
||||
laneRow.waitForExistence(timeout: Self.uiTimeout),
|
||||
"the \"\(RichBoard.firstLane)\" lane row never appeared"
|
||||
)
|
||||
laneRow.tap()
|
||||
|
||||
let cardRow = element(labelContaining: RichBoard.firstLaneFirstCard)
|
||||
XCTAssertTrue(
|
||||
cardRow.waitForExistence(timeout: Self.uiTimeout),
|
||||
"the \"\(RichBoard.firstLaneFirstCard)\" card row never appeared"
|
||||
)
|
||||
cardRow.tap()
|
||||
}
|
||||
|
||||
/// Scrolls the frontmost view upward, a little at a time, until `element` is hittable or
|
||||
/// `maxAttempts` is exhausted. A `Form`/`List` does not auto-scroll to reveal what a test asks
|
||||
/// for, and this suite's Settings screen puts the About section — everything
|
||||
|
||||
Reference in New Issue
Block a user