MastodonUISnapshotTests.swift 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. //
  2. // MastodonUISnapshotTests.swift
  3. // MastodonUITests
  4. //
  5. // Created by MainasuK on 2022-3-2.
  6. //
  7. import XCTest
  8. extension UInt64 {
  9. static let second: UInt64 = 1_000_000_000
  10. }
  11. @MainActor
  12. class MastodonUISnapshotTests: XCTestCase {
  13. override func setUpWithError() throws {
  14. // Put setup code here. This method is called before the invocation of each test method in the class.
  15. }
  16. override func tearDownWithError() throws {
  17. // Put teardown code here. This method is called after the invocation of each test method in the class.
  18. }
  19. override class func tearDown() {
  20. super.tearDown()
  21. let app = XCUIApplication()
  22. print(app.debugDescription)
  23. }
  24. }
  25. extension MastodonUISnapshotTests {
  26. func testSmoke() async throws {
  27. // This is an example of a functional test case.
  28. // Use XCTAssert and related functions to verify your tests produce the correct results.
  29. // Any test you write for XCTest can be annotated as throws and async.
  30. // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.
  31. // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.
  32. }
  33. }
  34. extension MastodonUISnapshotTests {
  35. func takeSnapshot(name: String) {
  36. let snapshot = XCUIScreen.main.screenshot()
  37. let attachment = XCTAttachment(
  38. uniformTypeIdentifier: "public.png",
  39. name: "\(name).\(UIDevice.current.name).png",
  40. payload: snapshot.pngRepresentation,
  41. userInfo: nil
  42. )
  43. attachment.lifetime = .keepAlways
  44. add(attachment)
  45. }
  46. // make tab display by tap it
  47. private func tapTab(app: XCUIApplication, tab: String) {
  48. let searchTab = app.tabBars.buttons[tab]
  49. if searchTab.exists { searchTab.tap() }
  50. let searchCell = app.collectionViews.cells[tab]
  51. if searchCell.exists { searchCell.tap() }
  52. }
  53. private func showTitleButtonMenu(app: XCUIApplication) async throws {
  54. let titleButton = app.navigationBars.buttons["TitleButton"].firstMatch
  55. XCTAssert(titleButton.waitForExistence(timeout: 5))
  56. titleButton.press(forDuration: 1.0)
  57. try await Task.sleep(nanoseconds: .second * 1)
  58. }
  59. private func snapshot(
  60. name: String,
  61. count: Int = 3,
  62. task: (_ app: XCUIApplication) async throws -> Void
  63. ) async rethrows {
  64. var app = XCUIApplication()
  65. // pass -1 to debug test case
  66. guard count >= 0 else {
  67. app.launch()
  68. try await task(app)
  69. takeSnapshot(name: name)
  70. return
  71. }
  72. // Light Mode
  73. for index in 0..<count {
  74. app.launch()
  75. try await task(app)
  76. let name = "\(name).light.\(index+1)"
  77. takeSnapshot(name: name)
  78. }
  79. // Dark Mode
  80. app = XCUIApplication()
  81. app.launchArguments.append("UIUserInterfaceStyleForceDark")
  82. for index in 0..<count {
  83. app.launch()
  84. try await task(app)
  85. let name = "\(name).dark.\(index+1)"
  86. takeSnapshot(name: name)
  87. }
  88. }
  89. }
  90. // MARK: - Home
  91. extension MastodonUISnapshotTests {
  92. func testSnapshotHome() async throws {
  93. try await snapshot(name: "Home") { app in
  94. tapTab(app: app, tab: "Home")
  95. try await Task.sleep(nanoseconds: .second * 3)
  96. }
  97. }
  98. }
  99. // MARK: - Thread
  100. extension MastodonUISnapshotTests {
  101. func testSnapshotThread() async throws {
  102. try await snapshot(name: "Thread") { app in
  103. let threadID = ProcessInfo.processInfo.environment["thread_id"]!
  104. try await coordinateToThread(app: app, id: threadID)
  105. try await Task.sleep(nanoseconds: .second * 5)
  106. }
  107. }
  108. // use debug entry goto thread scene by thread ID
  109. // assert the thread ID is valid for current sign in user server
  110. private func coordinateToThread(app: XCUIApplication, id: String) async throws {
  111. try await Task.sleep(nanoseconds: .second * 1)
  112. try await showTitleButtonMenu(app: app)
  113. let showMenu = app.collectionViews.buttons["Show…"].firstMatch
  114. XCTAssert(showMenu.waitForExistence(timeout: 3))
  115. showMenu.tap()
  116. try await Task.sleep(nanoseconds: .second * 1)
  117. let threadAction = app.collectionViews.buttons["Thread"].firstMatch
  118. XCTAssert(threadAction.waitForExistence(timeout: 3))
  119. threadAction.tap()
  120. try await Task.sleep(nanoseconds: .second * 1)
  121. let textField = app.alerts.textFields.firstMatch
  122. XCTAssert(textField.waitForExistence(timeout: 3))
  123. textField.typeText(id)
  124. try await Task.sleep(nanoseconds: .second * 1)
  125. let showAction = app.alerts.buttons["Show"].firstMatch
  126. XCTAssert(showAction.waitForExistence(timeout: 3))
  127. showAction.tap()
  128. try await Task.sleep(nanoseconds: .second * 1)
  129. }
  130. }
  131. // MARK: - Profile
  132. extension MastodonUISnapshotTests {
  133. func testSnapshotProfile() async throws {
  134. try await snapshot(name: "Profile") { app in
  135. let profileID = ProcessInfo.processInfo.environment["profile_id"]!
  136. try await coordinateToProfile(app: app, id: profileID)
  137. try await Task.sleep(nanoseconds: .second * 5)
  138. }
  139. }
  140. // use debug entry goto thread scene by profile ID
  141. // assert the profile ID is valid for current sign in user server
  142. private func coordinateToProfile(app: XCUIApplication, id: String) async throws {
  143. try await Task.sleep(nanoseconds: .second * 1)
  144. try await showTitleButtonMenu(app: app)
  145. let showMenu = app.collectionViews.buttons["Show…"].firstMatch
  146. XCTAssert(showMenu.waitForExistence(timeout: 3))
  147. showMenu.tap()
  148. try await Task.sleep(nanoseconds: .second * 1)
  149. let profileAction = app.collectionViews.buttons["Profile"].firstMatch
  150. XCTAssert(profileAction.waitForExistence(timeout: 3))
  151. profileAction.tap()
  152. try await Task.sleep(nanoseconds: .second * 1)
  153. let textField = app.alerts.textFields.firstMatch
  154. XCTAssert(textField.waitForExistence(timeout: 3))
  155. textField.typeText(id)
  156. try await Task.sleep(nanoseconds: .second * 1)
  157. let showAction = app.alerts.buttons["Show"].firstMatch
  158. XCTAssert(showAction.waitForExistence(timeout: 3))
  159. showAction.tap()
  160. try await Task.sleep(nanoseconds: .second * 1)
  161. }
  162. }
  163. // MARK: - Server Rules
  164. extension MastodonUISnapshotTests {
  165. func testSnapshotServerRules() async throws {
  166. try await snapshot(name: "ServerRules") { app in
  167. let domain = "mastodon.social"
  168. try await coordinateToOnboarding(app: app, page: .serverRules(domain: domain))
  169. try await Task.sleep(nanoseconds: .second * 3)
  170. }
  171. }
  172. }
  173. // MARK: - Search
  174. extension MastodonUISnapshotTests {
  175. func testSnapshotSearch() async throws {
  176. try await snapshot(name: "ServerRules") { app in
  177. tapTab(app: app, tab: "Search")
  178. try await Task.sleep(nanoseconds: .second * 3)
  179. }
  180. }
  181. }
  182. // MARK: - Compose
  183. extension MastodonUISnapshotTests {
  184. func testSnapshotCompose() async throws {
  185. try await snapshot(name: "Compose") { app in
  186. // open Compose scene
  187. let composeBarButtonItem = app.navigationBars.buttons["Compose"].firstMatch
  188. let composeCollectionViewCell = app.collectionViews.cells["Compose"]
  189. if composeBarButtonItem.waitForExistence(timeout: 5) {
  190. composeBarButtonItem.tap()
  191. } else if composeCollectionViewCell.waitForExistence(timeout: 5) {
  192. composeCollectionViewCell.tap()
  193. } else {
  194. XCTFail()
  195. }
  196. // type text
  197. let textView = app.textViews.firstMatch
  198. XCTAssert(textView.waitForExistence(timeout: 5))
  199. textView.tap()
  200. textView.typeText("Look at that view! #Athens ")
  201. // tap Add Attachment toolbar button
  202. let addAttachmentButton = app.buttons["Add Attachment"].firstMatch
  203. XCTAssert(addAttachmentButton.waitForExistence(timeout: 5))
  204. addAttachmentButton.tap()
  205. // tap Browse menu action to add stub image
  206. let browseButton = app.buttons["Browse"].firstMatch
  207. XCTAssert(browseButton.waitForExistence(timeout: 5))
  208. browseButton.tap()
  209. try await Task.sleep(nanoseconds: .second * 10)
  210. }
  211. }
  212. }
  213. // MARK: Sign in
  214. extension MastodonUISnapshotTests {
  215. // Please check the Documentation/Snapshot.md and run this test case in the command line
  216. func testSignInAccount() async throws {
  217. guard let domain = ProcessInfo.processInfo.environment["login_domain"] else {
  218. fatalError("env 'login_domain' missing")
  219. }
  220. guard let email = ProcessInfo.processInfo.environment["login_email"] else {
  221. fatalError("env 'login_email' missing")
  222. }
  223. guard let password = ProcessInfo.processInfo.environment["login_password"] else {
  224. fatalError("env 'login_password' missing")
  225. }
  226. try await signInApplication(
  227. domain: domain,
  228. email: email,
  229. password: password
  230. )
  231. }
  232. func signInApplication(
  233. domain: String,
  234. email: String,
  235. password: String
  236. ) async throws {
  237. let app = XCUIApplication()
  238. app.launch()
  239. try await coordinateToOnboarding(app: app, page: .login(domain: domain))
  240. // wait OAuth webpage display
  241. try await Task.sleep(nanoseconds: .second * 10)
  242. let webview = app.webViews.firstMatch
  243. XCTAssert(webview.waitForExistence(timeout: 10))
  244. func tapAuthorizeButton() async throws -> Bool {
  245. let authorizeButton = webview.buttons["AUTHORIZE"].firstMatch
  246. if authorizeButton.exists {
  247. authorizeButton.tap()
  248. try await Task.sleep(nanoseconds: .second * 5)
  249. return true
  250. }
  251. return false
  252. }
  253. let isAuthorized = try await tapAuthorizeButton()
  254. if !isAuthorized {
  255. let emailTextField = webview.textFields["E-mail address"].firstMatch
  256. XCTAssert(emailTextField.waitForExistence(timeout: 10))
  257. emailTextField.tap()
  258. emailTextField.typeText(email)
  259. let passwordTextField = webview.secureTextFields["Password"].firstMatch
  260. XCTAssert(passwordTextField.waitForExistence(timeout: 3))
  261. passwordTextField.tap()
  262. passwordTextField.typeText(password)
  263. let goKeyboardButton = XCUIApplication().keyboards.buttons["Go"].firstMatch
  264. XCTAssert(goKeyboardButton.waitForExistence(timeout: 3))
  265. goKeyboardButton.tap()
  266. var retry = 0
  267. let retryLimit = 20
  268. while webview.exists {
  269. guard retry < retryLimit else {
  270. fatalError("Cannot complete OAuth process")
  271. }
  272. retry += 1
  273. // will break due to webview dismiss
  274. _ = try await tapAuthorizeButton()
  275. print("Please enter the sign-in confirm code. Retry in 5s")
  276. try await Task.sleep(nanoseconds: .second * 5)
  277. }
  278. } else {
  279. // Done
  280. }
  281. print("OAuth finish")
  282. }
  283. enum OnboardingPage {
  284. case welcome
  285. case login(domain: String)
  286. case serverRules(domain: String)
  287. }
  288. private func coordinateToOnboarding(app: XCUIApplication, page: OnboardingPage) async throws {
  289. // check in Onboarding or not
  290. let loginButton = app.buttons["Log In"].firstMatch
  291. try await Task.sleep(nanoseconds: .second * 3)
  292. let loginButtonExists = loginButton.exists
  293. // goto Onboarding scene if already sign-in
  294. if !loginButtonExists {
  295. try await showTitleButtonMenu(app: app)
  296. let showMenu = app.collectionViews.buttons["Show…"].firstMatch
  297. XCTAssert(showMenu.waitForExistence(timeout: 3))
  298. showMenu.tap()
  299. try await Task.sleep(nanoseconds: .second * 1)
  300. let welcomeAction = app.collectionViews.buttons["Welcome"].firstMatch
  301. XCTAssert(welcomeAction.waitForExistence(timeout: 3))
  302. welcomeAction.tap()
  303. try await Task.sleep(nanoseconds: .second * 1)
  304. }
  305. func type(domain: String) async throws {
  306. // type domain
  307. let domainTextField = app.textFields.firstMatch
  308. XCTAssert(domainTextField.waitForExistence(timeout: 5))
  309. domainTextField.tap()
  310. // Skip system keyboard swipe input guide
  311. try await skipKeyboardSwipeInputGuide(app: app)
  312. domainTextField.typeText(domain)
  313. XCUIApplication().keyboards.buttons["Done"].firstMatch.tap()
  314. }
  315. switch page {
  316. case .welcome:
  317. break
  318. case .login(let domain):
  319. // Tap login button
  320. XCTAssert(loginButtonExists)
  321. loginButton.tap()
  322. // type domain
  323. try await type(domain: domain)
  324. // add system alert monitor
  325. // A. The monitor not works
  326. // addUIInterruptionMonitor(withDescription: "Authentication Alert") { alert in
  327. // alert.buttons["Continue"].firstMatch.tap()
  328. // return true
  329. // }
  330. // tap next
  331. try await selectServerAndContinue(app: app, domain: domain)
  332. // wait authentication alert display
  333. try await Task.sleep(nanoseconds: .second * 3)
  334. // B. Workaround
  335. let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
  336. let continueButton = springboard.buttons["Continue"].firstMatch
  337. XCTAssert(continueButton.waitForExistence(timeout: 3))
  338. continueButton.tap()
  339. case .serverRules(let domain):
  340. // Tap sign up button
  341. let signUpButton = app.buttons["Get Started"].firstMatch
  342. XCTAssert(signUpButton.waitForExistence(timeout: 3))
  343. signUpButton.tap()
  344. // type domain
  345. try await type(domain: domain)
  346. // tap next
  347. try await selectServerAndContinue(app: app, domain: domain)
  348. }
  349. }
  350. private func selectServerAndContinue(app: XCUIApplication, domain: String) async throws {
  351. // wait searching
  352. try await Task.sleep(nanoseconds: .second * 3)
  353. // tap server
  354. let cell = app.cells.containing(.staticText, identifier: domain).firstMatch
  355. XCTAssert(cell.waitForExistence(timeout: 5))
  356. cell.tap()
  357. // tap next button
  358. let nextButton = app.buttons.matching(NSPredicate(format: "enabled == true")).matching(identifier: "Next").firstMatch
  359. XCTAssert(nextButton.waitForExistence(timeout: 3))
  360. nextButton.tap()
  361. }
  362. private func skipKeyboardSwipeInputGuide(app: XCUIApplication) async throws {
  363. let swipeInputLabel = app.staticTexts["Speed up your typing by sliding your finger across the letters to compose a word."].firstMatch
  364. try await Task.sleep(nanoseconds: .second * 3)
  365. guard swipeInputLabel.exists else { return }
  366. let continueButton = app.buttons["Continue"]
  367. continueButton.tap()
  368. }
  369. }