import XCTest /// Playing an episode has to leave the page, reach the host's player, and be written back. /// /// Point it at the daemon the browser suite uses, which has fixture feeds and real files: /// /// node -e "require('./tests/ui/global-setup').prepare()" # in ipodderx-rs /// node tests/ui/fixtures/serve.js & /// IPX_CONFIG=$ROOT/config/config.toml IPX_DATA_DIR=$ROOT/data ./target/debug/ipx daemon & /// TEST_RUNNER_IPX_SERVER="http://127.0.0.1:8791/?token=" xcodebuild test ... /// /// TEST_RUNNER_ is not decoration: the test runs in its own process on the simulator, and that /// prefix is what carries a variable across to it. final class PlaybackTests: XCTestCase { /// Not "First Episode". The fixture caps downloads at one per scan and takes the newest, so /// the second is the one with a file behind it -- and an episode with nothing downloaded /// offers Download rather than Play. let feed = "test-show", guid = "ui-2", title = "Second Episode" 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 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() // 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. 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") 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. 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") } private func entry(in server: URL) -> [String: Any]? { var components = URLComponents(url: server, resolvingAgainstBaseURL: false)! components.path = "/api/entries" guard let url = components.url, let data = try? Data(contentsOf: url), let page = try? JSONSerialization.jsonObject(with: data) as? [String: Any], let entries = page["entries"] as? [[String: Any]] else { return nil } return entries.first { $0["guid"] as? String == guid } } /// Polls until the server says the episode is read. Its own request, not the app's -- the /// point is to ask the server what it ended up with. private func waitForRead(in server: URL, timeout: TimeInterval) -> Bool { let deadline = Date().addingTimeInterval(timeout) while Date() < deadline { if entry(in: server)?["read"] as? Bool == true { return true } Thread.sleep(forTimeInterval: 1) } return false } }