import XCTest /// Draws the screens `ios/look.mjs` asks for. It's that script's hands, not a /// test: with no plan it's skipped. final class LookTests: XCTestCase { private struct Plan: Decodable { let answers: String let misses: String let now: String let signedIn: Bool let runs: [Run] } private struct Run: Decodable { let route: String let scheme: String let theme: String? let steps: [Step] let top: Bool let out: String } private struct Step: Decodable { var tap: String? var type: String? var swipe: String? } func testLook() throws { guard let path = ProcessInfo.processInfo.environment["KNOTLESS_LOOK"] else { throw XCTSkip("ios/look.mjs names the plan") } let plan = try JSONDecoder().decode(Plan.self, from: Data(contentsOf: URL(fileURLWithPath: path))) continueAfterFailure = true for run in plan.runs { let app = XCUIApplication() app.launchEnvironment = [ "KNOTLESS_ANSWERS": plan.answers, "KNOTLESS_MISSES": plan.misses, "KNOTLESS_NOW": plan.now, "KNOTLESS_ROUTE": run.route, "KNOTLESS_SCHEME": run.scheme, ] if plan.signedIn { app.launchEnvironment["KNOTLESS_SIGNED_IN"] = "1" } if let theme = run.theme { app.launchEnvironment["KNOTLESS_THEME"] = theme } // From nothing this simulator's app was last left in, as a picture holds only to what it's asked. app.launchEnvironment["KNOTLESS_FRESH"] = "1" app.launch() sleep(2) for step in run.steps { take(step, in: app) } sleep(1) var last = shot(to: "\(run.out).png") guard !run.top else { continue } for page in 2...8 { app.swipeUp() // Till the scroll bar has gone, or the end looks like a move. sleep(3) let now = XCUIScreen.main.screenshot().pngRepresentation if now == last { break } try? now.write(to: URL(fileURLWithPath: "\(run.out)-\(page).png")) last = now } } } private func shot(to path: String) -> Data { let png = XCUIScreen.main.screenshot().pngRepresentation try? png.write(to: URL(fileURLWithPath: path)) return png } private func take(_ step: Step, in app: XCUIApplication) { if let label = step.tap, label.hasPrefix("@") { let xy = label.dropFirst().split(separator: ",").compactMap { Double($0) } app.coordinate(withNormalizedOffset: .zero).withOffset(CGVector(dx: xy[0], dy: xy[1])).tap() sleep(2) } else if let label = step.tap { let match = NSPredicate(format: "label == %@ OR identifier == %@", label, label) let found = app.descendants(matching: .any).matching(match).firstMatch for _ in 0..<8 where !(found.exists && found.isHittable) { app.swipeUp() } guard found.exists, found.isHittable else { XCTFail("LOOK: nothing to tap called \(label)") return } found.tap() sleep(1) } if let text = step.type { let box = app.textViews.firstMatch.exists ? app.textViews.firstMatch : app.textFields.firstMatch guard box.waitForExistence(timeout: 5) else { XCTFail("LOOK: nothing to type into") return } box.tap() box.typeText(text) } if step.swipe == "up" { app.swipeUp() } if step.swipe == "down" { app.swipeDown() } if step.swipe == "left" { app.swipeLeft() } if step.swipe == "right" { app.swipeRight() } } }