Date: 2026-03-16
Tester model: Claude Opus 4.6 (Tester mode)
Scope: Full app UI test coverage assessment
UITests/MixBoardUITests.swift — 20 test methods| Area | Tests | What's Covered |
|---|---|---|
| App Launch | 2 | Navigation title, toolbar buttons |
| Empty State | 1 | No-playlists message |
| Playlist CRUD | 4 | Create, cancel, multiple creates, delete via swipe |
| Playlist Navigation | 2 | Navigate to detail, header/track count |
| Library | 2 | Open sheet, browse mode buttons |
| Settings | 4 | Open sheet, skin section, mix targets, skin switch, library stats |
| Mini Player | 1 | Not visible without track |
| Now Playing | 1 | Not shown initially |
| Orientation | 1 | Landscape rotation no crash |
| Performance | 1 | Launch metric |
| Area | Gap | Risk |
|---|---|---|
| Cloud Browser | Zero tests | HIGH — the Years decoding bug lived here undetected |
| Cloud → Category drill-down | Zero tests | HIGH — all API-dependent navigation untested |
| Cloud → Album → Tracks | Zero tests | HIGH |
| Playback flow | Zero tests | MEDIUM — play a track, verify mini player appears |
| Queue management | Zero tests | MEDIUM — add to queue, reorder, remove |
| Now Playing transport | Zero tests | LOW — play/pause/skip buttons exist but untested |
| Lyrics panel | Zero tests | LOW |
| Settings: Chad Music config | Zero tests | MEDIUM — server URL, API key, test connection |
| Add to Playlist sheet | Zero tests | MEDIUM |
| Search (in lists) | Zero tests | LOW |
The app already has accessibilityIdentifier on key elements:
PlaylistListView: libraryButton, cloudBrowserButton, settingsButton, newPlaylistButton, playlistList, emptyState, playlistRow_{name}NowPlayingView: nowPlayingDismiss, queueButton, lyricsButton, nowPlayingTitle, nowPlayingArtist, shuffleButton, previousButton, playPauseButton, nextButton, lyricsPanelMiniPlayerView: miniPlayer, miniPlayerPlayPause, miniPlayerNext, miniPlayerQueueContentView: ContentViewMissing identifiers: CloudBrowserView has no accessibility identifiers at all — they'd need to be added before UI tests can target cloud elements.
-UITesting launch argument is passed in setUp() but never checked in app code — there's no mock data path or conditional behavior for UI testingChadMusicAPIClient is a concrete singleton (ChadMusicAPIClient.shared) with no protocol abstraction — cannot be mocked without changesThree viable options, ranked by effort and value:
How: Register a custom URLProtocol subclass that intercepts outgoing requests and returns canned JSON. Activated via the -UITesting launch argument.
Pros: No external dependencies, runs offline, deterministic, fast
Cons: Requires adding ~100 lines of stub infrastructure to the app target + JSON fixture files
Catches the Years bug?: YES — if fixtures match real API shape, decoding failures surface immediately. If fixtures are fabricated, they might mask the problem. Best practice: capture real API responses as fixtures.
How: Point UI tests at the actual Chad Music server (pre-configured URL + API key in test scheme environment variables).
Pros: Zero mock infrastructure, tests real behavior end-to-end
Cons: Tests are flaky (server down → tests fail), slow (network latency), non-deterministic (library content changes), can't run in CI without server access
Catches the Years bug?: YES — directly, because it hits the real API
How: Spin up a lightweight Swift HTTP server in the test setUp that serves canned responses.
Pros: Full control, realistic HTTP layer
Cons: High complexity, process management in XCUITest is cumbersome
Catches the Years bug?: Only if mock data matches real API shapes
Start with Option B (real server) for immediate smoke tests that catch bugs like the Years issue. Migrate to Option A (URLProtocol stubs) once you want CI reliability and offline testing. Option A requires the app to check for -UITesting and inject a stub URLSession into the API client.
testSwitchSkin() partially covers thisCould a UI test have caught it? YES, trivially.
The bug is in CategoryDetailView.load() which calls ChadMusicAPIClient.shared.fetchCategory(.year). The API returns year values in a format that ChadCategory.item: String can't decode. The view catches the error and displays errorMessage — so a UI test that navigates to Years and asserts "no error text is visible" would catch it immediately.
func testCloudBrowserYearsCategoryLoads() {
// Precondition: app is configured with a valid Chad Music server
let cloudButton = app.buttons["cloudBrowserButton"]
XCTAssertTrue(cloudButton.waitForExistence(timeout: 5))
cloudButton.tap()
// Navigate to the category list
let yearsCell = app.staticTexts["Years"]
XCTAssertTrue(yearsCell.waitForExistence(timeout: 5),
"Years category should appear in cloud browser")
yearsCell.tap()
// Wait for the category to load — should show items, not an error
// The view shows a red error text on decode failure
let loadingIndicator = app.activityIndicators.firstMatch
// Wait for loading to finish
let loaded = NSPredicate(format: "exists == false")
expectation(for: loaded, evaluatedWith: loadingIndicator, handler: nil)
waitForExpectations(timeout: 10)
// Verify: no error message visible
let errorTexts = app.staticTexts.matching(
NSPredicate(format: "label CONTAINS 'Failed to decode'")
)
XCTAssertEqual(errorTexts.count, 0,
"Years category should load without decoding errors")
// Verify: at least one year item exists
let listCells = app.cells
XCTAssertTrue(listCells.count > 0,
"Years list should have at least one item")
}
func testCloudBrowserAllCategoriesLoad() {
let cloudButton = app.buttons["cloudBrowserButton"]
XCTAssertTrue(cloudButton.waitForExistence(timeout: 5))
cloudButton.tap()
// Verify the browse section exists
let browseSection = app.staticTexts["Browse"]
XCTAssertTrue(browseSection.waitForExistence(timeout: 5))
// Test each non-album category (album has its own view)
let categories = ["Artists", "Genres", "Years"]
for categoryName in categories {
let cell = app.staticTexts[categoryName]
XCTAssertTrue(cell.waitForExistence(timeout: 3),
"\(categoryName) should be visible")
cell.tap()
// Wait for load, check no error
sleep(3) // Allow network request to complete
let errorTexts = app.staticTexts.matching(
NSPredicate(format: "label CONTAINS 'Failed' OR label CONTAINS 'Error'")
)
XCTAssertEqual(errorTexts.count, 0,
"\(categoryName) should load without errors")
// Navigate back
app.navigationBars.buttons.element(boundBy: 0).tap()
}
}
func testCloudTrackPlaybackShowsMiniPlayer() {
let cloudButton = app.buttons["cloudBrowserButton"]
XCTAssertTrue(cloudButton.waitForExistence(timeout: 5))
cloudButton.tap()
// Navigate: Albums → first album → first track
let albumsCell = app.staticTexts["Albums"]
XCTAssertTrue(albumsCell.waitForExistence(timeout: 5))
albumsCell.tap()
// Wait for albums to load, tap first one
let firstAlbum = app.cells.firstMatch
XCTAssertTrue(firstAlbum.waitForExistence(timeout: 10))
firstAlbum.tap()
// Wait for tracks to load, tap first one
let firstTrack = app.cells.firstMatch
XCTAssertTrue(firstTrack.waitForExistence(timeout: 10))
firstTrack.tap()
// Dismiss cloud browser
let doneButton = app.buttons["Done"]
if doneButton.waitForExistence(timeout: 2) {
doneButton.tap()
}
// Verify mini player appears
let miniPlayer = app.otherElements["miniPlayer"]
XCTAssertTrue(miniPlayer.waitForExistence(timeout: 10),
"Mini player should appear after tapping a cloud track")
}
| Phase | Tests | Complexity | Notes |
|---|---|---|---|
| Phase 1: Cloud smoke tests (P1) | 4 tests | Low | Requires: add accessibilityIdentifiers to CloudBrowserView, have a configured server |
| Phase 2: Playback + queue (P2) | 4 tests | Medium | Need a playable track (cloud or local), state verification |
| Phase 3: URLProtocol stubs | Infrastructure | Medium | ~100 lines stub code + JSON fixtures, app-side -UITesting check |
| Phase 4: Nice-to-have (P3) | 3 tests | Low | Lyrics, search, additional settings |
-UITesting launch argument handling in app code-UITesting as a launch argument, but the app never checks ProcessInfo.processInfo.arguments for itChadMusicAPIClient.shared is used directly in views. No protocol, no dependency injection. To use URLProtocol stubs, you'd need either: (a) make the client accept a custom URLSession, or (b) register a global URLProtocolCloudStreamingTests.swift has thorough unit tests for ChadCategory, ChadAlbum, ChadTrack decoding. However, these test with fabricated JSON — if the real API returns a different shape (e.g., year as Int not String), unit tests wouldn't catch it. UI tests against the real API WOULD catch it.Feasibility: HIGH. The app already has good accessibility identifiers on most views, an existing UI test infrastructure that works, and a test target properly configured in project.yml. The main gap is cloud browser coverage and API dependency handling.
Highest-impact, lowest-effort win: Add 4 smoke tests (T1–T4) that navigate the cloud browser categories and verify no decode errors. These run against the real server, require only adding accessibilityIdentifiers to CloudBrowserView, and would have caught the Years bug before a human ever saw it.
Longer-term: Implement URLProtocol stubs so cloud browser tests run reliably in CI without a live server.