MixBoardUITests.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. import XCTest
  2. /// Comprehensive UI tests for MixBoard iOS.
  3. /// Tests cover: app launch, playlist CRUD, navigation, settings, and Now Playing.
  4. final class MixBoardUITests: XCTestCase {
  5. var app: XCUIApplication!
  6. override func setUpWithError() throws {
  7. continueAfterFailure = false
  8. app = XCUIApplication()
  9. app.launchArguments += ["-UITesting"]
  10. app.launch()
  11. }
  12. override func tearDownWithError() throws {
  13. app = nil
  14. }
  15. // MARK: - App Launch
  16. func testAppLaunchShowsMixBoardTitle() {
  17. // The main screen should show "MixBoard" as the navigation title
  18. let title = app.navigationBars["MixBoard"]
  19. XCTAssertTrue(title.waitForExistence(timeout: 5), "MixBoard navigation title should exist")
  20. }
  21. func testAppLaunchShowsToolbarButtons() {
  22. // Library button should exist
  23. let libraryButton = app.buttons["libraryButton"]
  24. XCTAssertTrue(libraryButton.waitForExistence(timeout: 5), "Library button should exist")
  25. // Settings button should exist
  26. let settingsButton = app.buttons["settingsButton"]
  27. XCTAssertTrue(settingsButton.exists, "Settings button should exist")
  28. // New playlist button should exist
  29. let newPlaylistButton = app.buttons["newPlaylistButton"]
  30. XCTAssertTrue(newPlaylistButton.exists, "New playlist button should exist")
  31. }
  32. // MARK: - Empty State
  33. func testEmptyStateVisible() {
  34. // On a fresh install with no playlists, the empty state should be visible
  35. let emptyState = app.staticTexts["No playlists yet"]
  36. if emptyState.waitForExistence(timeout: 3) {
  37. XCTAssertTrue(emptyState.exists)
  38. let subtitle = app.staticTexts["Create a playlist to start building your mix"]
  39. XCTAssertTrue(subtitle.exists)
  40. }
  41. // If playlists already exist from a previous test, that's OK — skip
  42. }
  43. // MARK: - Create Playlist
  44. func testCreatePlaylist() {
  45. let newPlaylistButton = app.buttons["newPlaylistButton"]
  46. XCTAssertTrue(newPlaylistButton.waitForExistence(timeout: 5))
  47. newPlaylistButton.tap()
  48. // Alert should appear with "New Playlist" title
  49. let alert = app.alerts["New Playlist"]
  50. XCTAssertTrue(alert.waitForExistence(timeout: 3), "New Playlist alert should appear")
  51. // Type a name
  52. let textField = alert.textFields["Playlist name"]
  53. XCTAssertTrue(textField.exists, "Playlist name text field should exist")
  54. textField.tap()
  55. textField.typeText("UI Test Playlist")
  56. // Tap Create
  57. alert.buttons["Create"].tap()
  58. // Verify the playlist appears in the list
  59. let playlistCell = app.staticTexts["UI Test Playlist"]
  60. XCTAssertTrue(playlistCell.waitForExistence(timeout: 3), "Created playlist should appear in the list")
  61. }
  62. func testCreatePlaylistCancel() {
  63. let newPlaylistButton = app.buttons["newPlaylistButton"]
  64. XCTAssertTrue(newPlaylistButton.waitForExistence(timeout: 5))
  65. newPlaylistButton.tap()
  66. let alert = app.alerts["New Playlist"]
  67. XCTAssertTrue(alert.waitForExistence(timeout: 3))
  68. // Cancel
  69. alert.buttons["Cancel"].tap()
  70. // Alert should dismiss
  71. XCTAssertFalse(alert.exists)
  72. }
  73. // MARK: - Playlist Navigation
  74. func testNavigateToPlaylistDetail() {
  75. // Create a playlist first
  76. createPlaylistViaUI(name: "Detail Test")
  77. // Tap it
  78. let cell = app.staticTexts["Detail Test"]
  79. XCTAssertTrue(cell.waitForExistence(timeout: 3))
  80. cell.tap()
  81. // Should navigate to detail view
  82. let detailView = app.otherElements["playlistDetailView"]
  83. XCTAssertTrue(detailView.waitForExistence(timeout: 3), "Playlist detail view should appear")
  84. }
  85. func testPlaylistDetailShowsHeader() {
  86. createPlaylistViaUI(name: "Header Test")
  87. let cell = app.staticTexts["Header Test"]
  88. XCTAssertTrue(cell.waitForExistence(timeout: 3))
  89. cell.tap()
  90. // Should show track count
  91. let trackCount = app.staticTexts["0 tracks"]
  92. XCTAssertTrue(trackCount.waitForExistence(timeout: 3), "Track count should show 0 tracks")
  93. }
  94. // MARK: - Delete Playlist
  95. func testDeletePlaylistViaSwipe() {
  96. createPlaylistViaUI(name: "Delete Me")
  97. let cell = app.staticTexts["Delete Me"]
  98. XCTAssertTrue(cell.waitForExistence(timeout: 3))
  99. // Swipe left to reveal delete
  100. cell.swipeLeft()
  101. // Tap delete
  102. let deleteButton = app.buttons["Delete"]
  103. if deleteButton.waitForExistence(timeout: 2) {
  104. deleteButton.tap()
  105. // Verify it's gone
  106. XCTAssertFalse(cell.waitForExistence(timeout: 2))
  107. }
  108. }
  109. // MARK: - Library Navigation
  110. func testOpenLibrarySheet() {
  111. let libraryButton = app.buttons["libraryButton"]
  112. XCTAssertTrue(libraryButton.waitForExistence(timeout: 5))
  113. libraryButton.tap()
  114. // Library view should appear
  115. let libraryView = app.otherElements["libraryView"]
  116. let libraryTitle = app.navigationBars["Library"]
  117. let appeared = libraryView.waitForExistence(timeout: 3) || libraryTitle.waitForExistence(timeout: 3)
  118. XCTAssertTrue(appeared, "Library should open as a sheet")
  119. }
  120. func testLibraryBrowseModes() {
  121. let libraryButton = app.buttons["libraryButton"]
  122. XCTAssertTrue(libraryButton.waitForExistence(timeout: 5))
  123. libraryButton.tap()
  124. // Check browse mode buttons exist
  125. let modes = ["Folders", "Songs", "Artists", "Albums", "Genres"]
  126. for mode in modes {
  127. let button = app.buttons[mode]
  128. if button.waitForExistence(timeout: 2) {
  129. XCTAssertTrue(button.exists, "\(mode) browse mode button should exist")
  130. }
  131. }
  132. }
  133. // MARK: - Settings Navigation
  134. func testOpenSettingsSheet() {
  135. let settingsButton = app.buttons["settingsButton"]
  136. XCTAssertTrue(settingsButton.waitForExistence(timeout: 5))
  137. settingsButton.tap()
  138. // Settings view should appear
  139. let settingsTitle = app.navigationBars["Settings"]
  140. XCTAssertTrue(settingsTitle.waitForExistence(timeout: 3), "Settings should open as a sheet")
  141. }
  142. func testSettingsShowsSkinSection() {
  143. let settingsButton = app.buttons["settingsButton"]
  144. XCTAssertTrue(settingsButton.waitForExistence(timeout: 5))
  145. settingsButton.tap()
  146. // Check that skin options are visible
  147. let skinSection = app.staticTexts["Skin"]
  148. XCTAssertTrue(skinSection.waitForExistence(timeout: 3), "Skin section should be visible")
  149. }
  150. func testSettingsShowsMixTargetsSection() {
  151. let settingsButton = app.buttons["settingsButton"]
  152. XCTAssertTrue(settingsButton.waitForExistence(timeout: 5))
  153. settingsButton.tap()
  154. let mixTargetsSection = app.staticTexts["Mix Targets"]
  155. XCTAssertTrue(mixTargetsSection.waitForExistence(timeout: 3), "Mix Targets section should be visible")
  156. }
  157. func testSwitchSkin() {
  158. let settingsButton = app.buttons["settingsButton"]
  159. XCTAssertTrue(settingsButton.waitForExistence(timeout: 5))
  160. settingsButton.tap()
  161. // Tap on a skin option
  162. let vinylButton = app.buttons.matching(NSPredicate(format: "label CONTAINS 'Vinyl'")).firstMatch
  163. if vinylButton.waitForExistence(timeout: 3) {
  164. vinylButton.tap()
  165. // Skin should now be selected (checkmark should appear)
  166. // We just verify no crash — theme switching is tested in unit tests
  167. }
  168. }
  169. func testSettingsShowsLibraryStats() {
  170. let settingsButton = app.buttons["settingsButton"]
  171. XCTAssertTrue(settingsButton.waitForExistence(timeout: 5))
  172. settingsButton.tap()
  173. // Scroll down to Library section
  174. let librarySection = app.staticTexts["Library"]
  175. if librarySection.waitForExistence(timeout: 2) {
  176. XCTAssertTrue(app.staticTexts["Tracks"].exists || app.staticTexts.matching(NSPredicate(format: "label CONTAINS 'Tracks'")).count > 0)
  177. }
  178. }
  179. // MARK: - Mini Player
  180. func testMiniPlayerNotVisibleWithoutTrack() {
  181. // When no track is playing, mini player should not be visible
  182. let miniPlayer = app.otherElements["miniPlayer"]
  183. XCTAssertFalse(miniPlayer.exists, "Mini player should not be visible without a playing track")
  184. }
  185. // MARK: - Now Playing
  186. func testNowPlayingNotShownInitially() {
  187. // Now Playing should not be shown on launch
  188. let nowPlayingTitle = app.staticTexts["nowPlayingTitle"]
  189. XCTAssertFalse(nowPlayingTitle.exists, "Now Playing should not be shown initially")
  190. }
  191. // MARK: - Multiple Playlist Operations
  192. func testCreateMultiplePlaylists() {
  193. createPlaylistViaUI(name: "Playlist Alpha")
  194. createPlaylistViaUI(name: "Playlist Beta")
  195. createPlaylistViaUI(name: "Playlist Gamma")
  196. XCTAssertTrue(app.staticTexts["Playlist Alpha"].waitForExistence(timeout: 3))
  197. XCTAssertTrue(app.staticTexts["Playlist Beta"].exists)
  198. XCTAssertTrue(app.staticTexts["Playlist Gamma"].exists)
  199. }
  200. // MARK: - Orientation (iPad)
  201. func testLandscapeDoesNotCrash() {
  202. XCUIDevice.shared.orientation = .landscapeLeft
  203. sleep(1)
  204. let title = app.navigationBars["MixBoard"]
  205. XCTAssertTrue(title.waitForExistence(timeout: 3))
  206. // Rotate back
  207. XCUIDevice.shared.orientation = .portrait
  208. sleep(1)
  209. XCTAssertTrue(title.waitForExistence(timeout: 3))
  210. }
  211. // MARK: - Helpers
  212. /// Create a playlist using the UI flow.
  213. private func createPlaylistViaUI(name: String) {
  214. let newPlaylistButton = app.buttons["newPlaylistButton"]
  215. guard newPlaylistButton.waitForExistence(timeout: 5) else {
  216. XCTFail("New playlist button not found")
  217. return
  218. }
  219. newPlaylistButton.tap()
  220. let alert = app.alerts["New Playlist"]
  221. guard alert.waitForExistence(timeout: 3) else {
  222. XCTFail("New Playlist alert not found")
  223. return
  224. }
  225. let textField = alert.textFields["Playlist name"]
  226. textField.tap()
  227. textField.typeText(name)
  228. alert.buttons["Create"].tap()
  229. // Wait for the playlist to appear
  230. _ = app.staticTexts[name].waitForExistence(timeout: 3)
  231. }
  232. }
  233. // MARK: - Launch Performance Test
  234. final class MixBoardLaunchPerformanceTests: XCTestCase {
  235. func testLaunchPerformance() throws {
  236. if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
  237. measure(metrics: [XCTApplicationLaunchMetric()]) {
  238. XCUIApplication().launch()
  239. }
  240. }
  241. }
  242. }