Comments, phase 3 — search, the thread find, announcements, and a11y
Board search reaches comment bodies through a search-owned transient
index: the first live-query keystroke sweeps comments/*/index.md
off-actor (.draft and comments/.trash excluded), keystrokes re-filter
in memory, the index discards on clear — the snapshot stays O(cards).
⌘F routes by focus: the comments pane gets an app-owned find bar
spanning the whole rendered thread (next/prev cross rows with
wraparound); body and composer keep NSTextFinder; Find Next/Previous
graduate from FutureCommands. Foreign comment changes speak
path-shaped beside the announcer's ladder ("New comment on 'X'",
plural folds), narrowed by EchoLedger receipts consumed through
CommentPath.classify — and that read fixed a latent footprint bug
where a comment receipt resolved against the card's attachment
listing, read .absent, and classified the user's own write as
foreign. The pane completes its a11y story: flattened comment
elements with Edit/Delete/Reveal custom actions (un-flattening
during inline edit), phrase-table vocabulary, labeled composer and
sort control, and an audit over the open pane on a comment-seeded
fixture (runnable only where automation permission exists).
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -199,6 +199,44 @@ struct UITestFixtureBoardTests {
|
||||
#expect(richCard.attachments == [UITestLaunch.attachmentName])
|
||||
}
|
||||
|
||||
/// The rich card's **comment thread** — what the comments-pane audit is an audit *of*
|
||||
/// (10-accessibility.md ▸ Comments).
|
||||
///
|
||||
/// The three shapes are the point: an ordinary comment, an unattributed one, and an edited one.
|
||||
/// Each drives a different branch of the author line a flattened comment element wears
|
||||
/// (`CommentAuthorLine`), and a fixture that quietly lost one of them would leave the audit
|
||||
/// passing over a thread of three identical rows.
|
||||
@Test("The rich card carries a thread the pane's audit can read")
|
||||
func fixtureSeedsAThread() throws {
|
||||
UITestLaunch.prepareScratchDirectory()
|
||||
defer { try? FileManager.default.removeItem(at: UITestLaunch.scratchRoot) }
|
||||
|
||||
let root = try UITestLaunch.materializeFixtureBoard()
|
||||
let model = try BoardLoader.load(boardRoot: root).model
|
||||
let lane = model.lanes[UITestLaunch.richCardIndex.lane]
|
||||
let card = try #require(lane.cards.first)
|
||||
let cardFolder = ItemPath.card(lane: lane.id, id: card.id).folder(under: root)
|
||||
|
||||
let thread = CommentThread.load(inCard: cardFolder, path: "\(lane.id.rawValue)/\(card.id.rawValue)")
|
||||
#expect(thread.comments.count == UITestLaunch.commentBodies.count)
|
||||
#expect(thread.strays.isEmpty)
|
||||
// Posting restamps `created`, so the thread's chronological order is the order they were made.
|
||||
#expect(thread.comments.map(\.body) != [])
|
||||
|
||||
let unattributed = try #require(
|
||||
thread.comments.first { $0.body.contains("no `author` key") }
|
||||
)
|
||||
#expect(unattributed.author.value == nil)
|
||||
#expect(!unattributed.isEdited)
|
||||
|
||||
let edited = try #require(thread.comments.first { $0.body == UITestLaunch.editedCommentBody })
|
||||
#expect(edited.isEdited, "the edited marker is `modified` differing from `created`, and no extra field")
|
||||
#expect(edited.author.value != nil)
|
||||
|
||||
// The board walk never loads any of it — the pane reads its own thread (01 ▸ Enhanced schema).
|
||||
#expect(!card.body.contains("specimen thread"))
|
||||
}
|
||||
|
||||
/// Everything the fixture launch writes stays inside the app's own container — the sandbox
|
||||
/// constraint that decided the whole design (a path handed over on the command line would not be
|
||||
/// readable), stated as a test so a future "just use `/tmp`" cannot land quietly.
|
||||
|
||||
Reference in New Issue
Block a user