| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216 |
- import XCTest
- /// Cloud browser smoke tests — verifies cloud music browsing flows.
- /// Requires Chad Music server to be running for Phase 1 tests.
- /// Phase 3 tests use `-MockNetwork` for CI-reliable offline testing.
- final class CloudBrowserUITests: XCTestCase {
- var app: XCUIApplication!
- override func setUpWithError() throws {
- continueAfterFailure = false
- app = XCUIApplication()
- app.launchArguments += ["-UITesting"]
- }
- override func tearDownWithError() throws {
- app = nil
- }
- // MARK: - Helpers
- /// Opens the cloud browser sheet by tapping the cloud button in the toolbar.
- private func openCloudBrowser() {
- let cloudButton = app.buttons["cloudBrowserButton"]
- XCTAssertTrue(cloudButton.waitForExistence(timeout: 5), "Cloud browser button should exist")
- cloudButton.tap()
- }
- // MARK: - Phase 1: Cloud Browser Smoke Tests (live server)
- /// Verifies the cloud browser opens and shows the Browse section with all category types.
- func testCloudHomeLoads() {
- app.launch()
- openCloudBrowser()
- // Browse section should appear
- let albumsLink = app.buttons["cloud.browse.album"]
- // Wait for a category link to appear
- let browseAppeared = albumsLink.waitForExistence(timeout: 5)
- // If server is configured, categories should appear
- if browseAppeared {
- // Verify key category types are listed
- let expectedCategories = ["album", "artist", "genre", "year"]
- for cat in expectedCategories {
- let link = app.buttons["cloud.browse.\(cat)"]
- XCTAssertTrue(link.exists, "\(cat) category should be listed in Browse section")
- }
- }
- // If not configured, the "Not Connected" view appears — that's OK for CI
- }
- /// Verifies each category type can be tapped and loads without showing error text.
- func testAllCategoriesLoad() {
- app.launch()
- openCloudBrowser()
- let albumsLink = app.buttons["cloud.browse.album"]
- guard albumsLink.waitForExistence(timeout: 5) else {
- // Server not configured, skip gracefully
- return
- }
- // Test non-album categories (album has its own view)
- let categories = ["artist", "genre", "year", "publisher"]
- for cat in categories {
- let link = app.buttons["cloud.browse.\(cat)"]
- guard link.waitForExistence(timeout: 3) else { continue }
- link.tap()
- // Wait for loading to complete
- let errorText = app.staticTexts.matching(identifier: "cloud.category.error").firstMatch
- let list = app.otherElements["cloud.category.list"]
- // Give it time to load
- _ = list.waitForExistence(timeout: 10)
- // Error text should NOT appear
- XCTAssertFalse(errorText.exists, "\(cat) category should load without error")
- // Navigate back
- app.navigationBars.buttons.firstMatch.tap()
- }
- }
- /// Verifies navigating to Albums → tapping an album → tracks appear.
- func testAlbumDetailOpens() {
- app.launch()
- openCloudBrowser()
- let albumsLink = app.buttons["cloud.browse.album"]
- guard albumsLink.waitForExistence(timeout: 5) else { return }
- albumsLink.tap()
- // Wait for album list to load
- let albumList = app.otherElements["cloud.albums.list"]
- guard albumList.waitForExistence(timeout: 10) else {
- XCTFail("Album list should load")
- return
- }
- // Tap the first album row
- let firstAlbum = app.cells.firstMatch
- guard firstAlbum.waitForExistence(timeout: 5) else {
- // No albums — server may be empty
- return
- }
- firstAlbum.tap()
- // Track list should appear
- let trackList = app.otherElements["cloud.albumDetail.trackList"]
- let header = app.otherElements["cloud.albumDetail.header"]
- let appeared = trackList.waitForExistence(timeout: 10) || header.waitForExistence(timeout: 10)
- XCTAssertTrue(appeared, "Album detail should show track list or header")
- }
- /// Verifies navigating Artists → tapping an artist → filtered albums appear.
- func testCategoryDrillDown() {
- app.launch()
- openCloudBrowser()
- let artistLink = app.buttons["cloud.browse.artist"]
- guard artistLink.waitForExistence(timeout: 5) else { return }
- artistLink.tap()
- // Wait for artist list to load
- let categoryList = app.otherElements["cloud.category.list"]
- guard categoryList.waitForExistence(timeout: 10) else {
- XCTFail("Artist category should load")
- return
- }
- // Tap the first artist
- let firstItem = app.cells.firstMatch
- guard firstItem.waitForExistence(timeout: 5) else { return }
- firstItem.tap()
- // Filtered album list or empty state should appear
- let filteredList = app.otherElements["cloud.filtered.list"]
- let emptyState = app.otherElements["cloud.filtered.empty"]
- let appeared = filteredList.waitForExistence(timeout: 10) || emptyState.waitForExistence(timeout: 10)
- XCTAssertTrue(appeared, "Category drill-down should show filtered albums or empty state")
- }
- // MARK: - Phase 3: CI-Reliable Offline Tests (mocked network)
- /// Launches app with mock network — verifies cloud browser renders with stubbed data.
- func testCloudBrowserWithMockData() {
- app.launchArguments += ["-MockNetwork"]
- app.launch()
- openCloudBrowser()
- // Stats should render from mock data (identifier propagates to StaticText children)
- let stats = app.staticTexts.matching(identifier: "cloud.stats").firstMatch
- XCTAssertTrue(stats.waitForExistence(timeout: 5), "Stats section should render with mock data")
- // Browse section categories should be visible
- let yearLink = app.buttons["cloud.browse.year"]
- XCTAssertTrue(yearLink.waitForExistence(timeout: 3), "Year category should be listed")
- // Tap Years — should load mock year data without error
- yearLink.tap()
- let errorText = app.staticTexts.matching(identifier: "cloud.category.error").firstMatch
- // Wait for category content to load
- sleep(2)
- XCTAssertFalse(errorText.exists, "Years should load without error from mock data")
- }
- /// Verifies that a malformed JSON response shows an error message to the user.
- func testDecodingErrorShowsMessage() {
- // The mock data is valid by default; to test decoding errors we'd need
- // a mechanism to inject bad data. For now, test with an unconfigured server
- // (no -MockNetwork, no real server) — the error path exercises the same UI.
- app.launch()
- // If server is NOT configured, opening cloud browser shows "Not Connected"
- openCloudBrowser()
- // Either "Not Connected" or the browse view should appear — no crash
- let notConnected = app.staticTexts["Not Connected"]
- let browseSection = app.buttons["cloud.browse.album"]
- let appeared = notConnected.waitForExistence(timeout: 5) || browseSection.waitForExistence(timeout: 5)
- XCTAssertTrue(appeared, "Should show either Not Connected or Browse section")
- }
- /// Verifies album tracks render correctly with mocked data.
- func testAlbumTracksWithMockData() {
- app.launchArguments += ["-MockNetwork"]
- app.launch()
- openCloudBrowser()
- // Navigate to Albums
- let albumsLink = app.buttons["cloud.browse.album"]
- guard albumsLink.waitForExistence(timeout: 5) else {
- XCTFail("Album category should be visible with mock data")
- return
- }
- albumsLink.tap()
- // Wait for mock album to appear (List renders as CollectionView on iOS)
- let firstAlbumRow = app.buttons["cloud.album.row.album-1"]
- guard firstAlbumRow.waitForExistence(timeout: 5) else {
- XCTFail("Album list should render with mock data")
- return
- }
- // Tap first album
- firstAlbumRow.tap()
- // Track list should appear (mock returns tracks with known titles)
- let trackTitle = app.staticTexts["First Track"]
- XCTAssertTrue(trackTitle.waitForExistence(timeout: 5), "Track list should render with mock data")
- }
- }
|