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 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 list never showed the fixture episode") // 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), "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, 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") } /// 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]? { 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 } }