PlaybackUITests.swift 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. import XCTest
  2. /// Playback and queue UI tests — verifies play, queue add, play next, and clear operations.
  3. /// Uses `-MockNetwork` to ensure tests work without a live server.
  4. final class PlaybackUITests: XCTestCase {
  5. var app: XCUIApplication!
  6. override func setUpWithError() throws {
  7. continueAfterFailure = false
  8. app = XCUIApplication()
  9. app.launchArguments += ["-UITesting", "-MockNetwork"]
  10. }
  11. override func tearDownWithError() throws {
  12. app = nil
  13. }
  14. // MARK: - Helpers
  15. private func openCloudBrowser() {
  16. let cloudButton = app.buttons["cloudBrowserButton"]
  17. XCTAssertTrue(cloudButton.waitForExistence(timeout: 5), "Cloud browser button should exist")
  18. cloudButton.tap()
  19. }
  20. /// Navigates to a mock album's track list.
  21. /// Returns true if tracks are visible.
  22. @discardableResult
  23. private func navigateToAlbumTracks() -> Bool {
  24. openCloudBrowser()
  25. let albumsLink = app.buttons["cloud.browse.album"]
  26. guard albumsLink.waitForExistence(timeout: 5) else { return false }
  27. albumsLink.tap()
  28. // Wait for mock album row to appear
  29. let firstAlbumRow = app.buttons["cloud.album.row.album-1"]
  30. guard firstAlbumRow.waitForExistence(timeout: 5) else { return false }
  31. firstAlbumRow.tap()
  32. // Wait for track content to load
  33. let trackTitle = app.staticTexts["First Track"]
  34. return trackTitle.waitForExistence(timeout: 5)
  35. }
  36. // MARK: - Phase 2: Playback Tests
  37. /// Play a cloud track → mini player should appear with the track title.
  38. func testPlayCloudTrack() {
  39. app.launch()
  40. guard navigateToAlbumTracks() else {
  41. XCTFail("Could not navigate to album tracks")
  42. return
  43. }
  44. // Tap the first track row (the "Play All" button or a track)
  45. let playAllButton = app.buttons["Play All"]
  46. if playAllButton.waitForExistence(timeout: 3) {
  47. playAllButton.tap()
  48. } else {
  49. // Tap first track cell
  50. let firstTrack = app.cells.element(boundBy: 2) // skip header + play all section
  51. if firstTrack.waitForExistence(timeout: 3) {
  52. firstTrack.tap()
  53. }
  54. }
  55. // Dismiss the cloud browser
  56. let doneButton = app.buttons["Done"]
  57. if doneButton.waitForExistence(timeout: 3) {
  58. doneButton.tap()
  59. }
  60. // Mini player should appear
  61. let miniPlayer = app.otherElements["miniPlayer"]
  62. XCTAssertTrue(miniPlayer.waitForExistence(timeout: 10), "Mini player should appear after playing a track")
  63. // Track title should be visible in mini player
  64. let trackTitle = app.staticTexts.matching(identifier: "miniPlayer.trackTitle").firstMatch
  65. XCTAssertTrue(trackTitle.exists, "Mini player should show the track title")
  66. }
  67. /// Long-press a track → "Add to Queue" → queue view shows the entry.
  68. func testAddToQueue() {
  69. app.launch()
  70. guard navigateToAlbumTracks() else {
  71. XCTFail("Could not navigate to album tracks")
  72. return
  73. }
  74. // Long-press on a track to open context menu
  75. let trackCell = app.cells.element(boundBy: 3) // skip header + play all rows
  76. guard trackCell.waitForExistence(timeout: 3) else {
  77. XCTFail("Track cell should exist")
  78. return
  79. }
  80. trackCell.press(forDuration: 1.2)
  81. // Tap "Add to Queue" from context menu
  82. let addToQueueButton = app.buttons["Add to Queue"]
  83. guard addToQueueButton.waitForExistence(timeout: 3) else {
  84. // Context menu might not appear in simulator, skip gracefully
  85. return
  86. }
  87. addToQueueButton.tap()
  88. // Dismiss cloud browser
  89. let doneButton = app.buttons["Done"]
  90. if doneButton.waitForExistence(timeout: 3) {
  91. doneButton.tap()
  92. }
  93. // Open queue view (via mini player queue button or Now Playing)
  94. // First play something so queue is accessible
  95. // Queue is a sheet — check if we can open it from the mini player
  96. let miniPlayerQueue = app.buttons["miniPlayerQueue"]
  97. if miniPlayerQueue.waitForExistence(timeout: 5) {
  98. miniPlayerQueue.tap()
  99. // Queue should show the added entry
  100. let queueEmpty = app.otherElements["queue.emptyState"]
  101. XCTAssertFalse(queueEmpty.exists, "Queue should not be empty after adding a track")
  102. }
  103. }
  104. /// Long-press → "Play Next" → track appears at top of queue.
  105. func testQueuePlayNext() {
  106. app.launch()
  107. guard navigateToAlbumTracks() else {
  108. XCTFail("Could not navigate to album tracks")
  109. return
  110. }
  111. // Play first track to establish a now-playing state
  112. let playAllButton = app.buttons["Play All"]
  113. if playAllButton.waitForExistence(timeout: 3) {
  114. playAllButton.tap()
  115. }
  116. // Wait for playback to start
  117. sleep(2)
  118. // Navigate back to tracks if needed
  119. let backButton = app.navigationBars.buttons.firstMatch
  120. if backButton.exists {
  121. // We may still be on the album detail — long-press a different track
  122. }
  123. // Long-press second track
  124. let trackCell = app.cells.element(boundBy: 4)
  125. guard trackCell.waitForExistence(timeout: 3) else { return }
  126. trackCell.press(forDuration: 1.2)
  127. let playNextButton = app.buttons["Play Next"]
  128. guard playNextButton.waitForExistence(timeout: 3) else { return }
  129. playNextButton.tap()
  130. // Dismiss cloud browser
  131. let doneButton = app.buttons["Done"]
  132. if doneButton.waitForExistence(timeout: 3) {
  133. doneButton.tap()
  134. }
  135. // Open queue
  136. let miniPlayerQueue = app.buttons["miniPlayerQueue"]
  137. if miniPlayerQueue.waitForExistence(timeout: 5) {
  138. miniPlayerQueue.tap()
  139. // Queue should have entries
  140. let queueEmpty = app.otherElements["queue.emptyState"]
  141. XCTAssertFalse(queueEmpty.exists, "Queue should have the 'Play Next' entry")
  142. }
  143. }
  144. /// Add to queue → Clear → queue should be empty.
  145. func testQueueClear() {
  146. app.launch()
  147. guard navigateToAlbumTracks() else {
  148. XCTFail("Could not navigate to album tracks")
  149. return
  150. }
  151. // Play a track first
  152. let playAllButton = app.buttons["Play All"]
  153. if playAllButton.waitForExistence(timeout: 3) {
  154. playAllButton.tap()
  155. }
  156. sleep(2)
  157. // Add another track to queue via context menu
  158. let trackCell = app.cells.element(boundBy: 4)
  159. guard trackCell.waitForExistence(timeout: 3) else { return }
  160. trackCell.press(forDuration: 1.2)
  161. let addToQueueButton = app.buttons["Add to Queue"]
  162. guard addToQueueButton.waitForExistence(timeout: 3) else { return }
  163. addToQueueButton.tap()
  164. // Dismiss cloud browser
  165. let doneButton = app.buttons["Done"]
  166. if doneButton.waitForExistence(timeout: 3) {
  167. doneButton.tap()
  168. }
  169. // Open queue
  170. let miniPlayerQueue = app.buttons["miniPlayerQueue"]
  171. guard miniPlayerQueue.waitForExistence(timeout: 5) else {
  172. XCTFail("Mini player queue button should exist")
  173. return
  174. }
  175. miniPlayerQueue.tap()
  176. // Tap Clear
  177. let clearButton = app.buttons["queue.clearButton"]
  178. if clearButton.waitForExistence(timeout: 3) {
  179. clearButton.tap()
  180. // Queue should be empty (only Now Playing remains)
  181. // The clear button itself should disappear after clearing
  182. XCTAssertFalse(clearButton.waitForExistence(timeout: 3), "Clear button should disappear after clearing queue")
  183. }
  184. }
  185. }