A native feed list and item list (#3)
The lists are native now, and the page is a button in the toolbar: it is still where signing in happens, and it is still the whole of settings, the admin page, the directory and OPML, which were never going to be rewritten. LibraryStore holds a page of items and asks for the next, because the sorting, filtering and searching are the server's work and ten thousand items have no business being in memory to be sorted here. Read and pinned are set locally and sent after, as the page's readWrites map does and for the same reason: a list asked for before the write lands answers with the old state, which put the dot back on an item just read. The interface follows the account's light or dark rather than the phone's, and defaults to dark when nobody has chosen, because that is what ipx's own theme.ts does. Following the system instead put a light list in front of a dark page. preferredColorScheme was not enough on its own -- inside a hosting controller it did not reach the hierarchy -- so the style is overridden on the controller, which also carries to the page presented over it. The sidebar had to be broken into sub-views: the whole list in one expression was more than the type checker would work through, and it said so rather than compiling it. Tested against the real library, 135 feeds and eleven thousand items, and the playback test now drives the native row rather than the page's button. Thirteen pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,39 +20,43 @@ final class PlaybackTests: XCTestCase {
|
||||
func testPlayingAnEpisodeReachesTheHostAndComesBack() throws {
|
||||
let server = ProcessInfo.processInfo.environment["IPX_SERVER"] ?? ""
|
||||
try XCTSkipIf(server.isEmpty, "set TEST_RUNNER_IPX_SERVER to a daemon with an episode downloaded")
|
||||
let base = try XCTUnwrap(URL(string: server))
|
||||
|
||||
// Unread first, so that becoming read again can only be playback's doing. Set through
|
||||
// the API rather than by driving the interface: what is being tested is the playing.
|
||||
try setRead(false, in: base)
|
||||
|
||||
let app = XCUIApplication()
|
||||
app.launchArguments = ["-ipx.server", server]
|
||||
app.launch()
|
||||
|
||||
let episode = app.staticTexts[title]
|
||||
XCTAssertTrue(episode.waitForExistence(timeout: 30), "the page never listed the fixture episode")
|
||||
episode.tap()
|
||||
XCTAssertTrue(episode.waitForExistence(timeout: 30), "the list never showed the fixture episode")
|
||||
|
||||
// On a phone the files sit inside the item; the row's own buttons are hidden by the
|
||||
// stylesheet, so this is the only play button on screen.
|
||||
// Unread first, so that becoming read again can only be playback's doing -- opening an
|
||||
// item marks it read by itself, which would otherwise answer the question before the
|
||||
// test asked it.
|
||||
let markUnread = app.buttons["Mark unread"].firstMatch
|
||||
if markUnread.waitForExistence(timeout: 10) { markUnread.tap() }
|
||||
|
||||
// On a phone the files sit inside the item; the row's own buttons are hidden by the
|
||||
// stylesheet, so this is the only play button on screen.
|
||||
// The row's own play button. Every row with a downloaded file has one.
|
||||
let play = app.buttons["Play"].firstMatch
|
||||
XCTAssertTrue(play.waitForExistence(timeout: 15),
|
||||
"the item offers no way to play it, so it has no downloaded file")
|
||||
"no row offers to play, so nothing in the list has a downloaded file")
|
||||
play.tap()
|
||||
|
||||
// Read again, rather than a position: the browser suite's fixture is a sixth of a second
|
||||
// long, and a position that rounds to zero is never written. Read is set at the end of an
|
||||
// episode, and inside the shell the page only learns an episode ended because the host
|
||||
// said so -- nothing here is playing the file itself.
|
||||
// long and a position that rounds to zero is never written. Read is set at the end of an
|
||||
// episode, by the host, which is the whole round trip.
|
||||
XCTAssertTrue(waitForRead(in: base, timeout: 30),
|
||||
"the episode never came back read, so the host never played it through")
|
||||
}
|
||||
|
||||
XCTAssertTrue(waitForRead(in: URL(string: server)!, timeout: 30),
|
||||
"the episode never came back read, so the host never played it through: "
|
||||
+ "either the message did not reach it, it could not fetch /media with the "
|
||||
+ "session cookie, or its answer never came back")
|
||||
/// Sets read through ipx directly, to put the fixture in a known state first.
|
||||
private func setRead(_ read: Bool, in server: URL) throws {
|
||||
var components = try XCTUnwrap(URLComponents(url: server, resolvingAgainstBaseURL: false))
|
||||
components.path = "/api/entries/\(feed)/\(guid)/flags"
|
||||
var req = URLRequest(url: try XCTUnwrap(components.url))
|
||||
req.httpMethod = "POST"
|
||||
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
req.httpBody = try JSONSerialization.data(withJSONObject: ["read": read])
|
||||
let done = expectation(description: "flags written")
|
||||
URLSession.shared.dataTask(with: req) { _, _, _ in done.fulfill() }.resume()
|
||||
wait(for: [done], timeout: 15)
|
||||
}
|
||||
|
||||
private func entry(in server: URL) -> [String: Any]? {
|
||||
|
||||
Reference in New Issue
Block a user