A native item pane, with the show notes still HTML (#4)

The reading pane: the item, its files with play, download and delete, read
and pin in the toolbar, a link to the original, and the notes.

The notes are a web view, deliberately. They are feed-supplied HTML that
ipx has already run through ammonia, and there is no good native renderer
for that -- NSAttributedString(html:) is slow, single-threaded and ugly,
and writing a real one is a project. So this carries only the notes, with
just enough stylesheet to belong to the app: the system font at body size,
the label colours, links in the accent. Nothing else is imposed, because
the markup is the publisher's. A link opens in Safari rather than inside
the view, where it would replace the notes with somebody's website and
leave no way back.

Deleting asks first, and says when the file is shared: one file serves
everyone reading the feed, so removing it is not a private act.

Two goes at the navigation. A row with a selection binding on the list
highlighted and went nowhere -- the binding takes the tap before the link
sees it. Without the binding, NavigationLink(value:) with a matching
navigationDestination still pushed nothing and gave no reason, so the link
carries its own destination now, which has nothing to get wrong.

Fourteen tests pass. The new one opens an item and looks for its notes, its
delete button and its read control, so a pane that renders empty fails
rather than passing quietly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-20 09:34:34 -04:00
parent 01d8bd936e
commit 292941524e
7 changed files with 304 additions and 9 deletions

View File

@@ -79,3 +79,30 @@ final class PlaybackTests: XCTestCase {
return false
}
}
/// Opening an item, which is a push away from the list and has to show what is in it.
final class ItemPaneTests: XCTestCase {
func testOpeningAnItemShowsItsFilesAndNotes() throws {
let server = ProcessInfo.processInfo.environment["IPX_SERVER"] ?? ""
try XCTSkipIf(server.isEmpty, "set TEST_RUNNER_IPX_SERVER to a fixture daemon")
let app = XCUIApplication()
app.launchArguments = ["-ipx.server", server]
app.launch()
XCTAssertTrue(app.staticTexts["Second Episode"].firstMatch.waitForExistence(timeout: 30),
"the list never appeared")
// The row, not its title: tapping the label does not activate the link, and with a
// selection binding on the list the tap was swallowed before the link ever saw it.
app.cells.element(boundBy: 0).tap()
// The pane is titled with the feed, and carries the item's file and its notes.
XCTAssertTrue(app.staticTexts["Notes for the second."].waitForExistence(timeout: 15),
"the show notes did not render")
XCTAssertTrue(app.buttons["Delete file"].firstMatch.exists,
"the downloaded file is not offered for deletion")
XCTAssertTrue(app.buttons["Mark unread"].firstMatch.exists
|| app.buttons["Mark read"].firstMatch.exists,
"no read control in the pane")
}
}