import XCTest /// One accessibility audit per primary screen, plus a Dynamic Type sweep. /// /// See `AccessibilityAuditHarness` for why these report rather than fail by /// default, and how to make them enforcing. @MainActor final class AccessibilityAuditTests: XCTestCase { override func setUp() { // Keep going after a failure so an enforced audit still collects and // attaches every finding. With this off, XCTest aborts at the first // reported issue and the burndown list is lost precisely when a category // is being enforced. continueAfterFailure = true } // MARK: Per-screen audits func testInspectScreen() throws { try auditRootTab("Inspect") } func testDashboardScreen() throws { try auditRootTab("Dashboard") } func testAuditScreen() throws { try auditRootTab("Audit") } func testHistoryScreen() throws { try auditRootTab("History") } func testSettingsScreen() throws { try auditRootTab("Settings") } func testTrackedDomainsScreen() throws { let app = AccessibilityAuditHarness.launch() app.selectRootTab("Settings") let trackedDomains = app.buttons["Tracked Domains"] XCTAssertTrue( trackedDomains.waitForExistence(timeout: 5), "Settings no longer offers a Tracked Domains row" ) trackedDomains.tap() let audited = try AccessibilityAuditHarness.audit(app, screen: "tracked-domains", test: self) try XCTSkipUnless(audited, "Tracked Domains audit did not complete in time") } // MARK: Dynamic Type /// Re-audits every root screen at the largest accessibility content size. /// /// This is where clipped text and fixed-height containers surface — the /// `.accessibility5`-class failures that the fixed geometry in /// `AppDensityMetrics` is expected to produce until phase 3 of #21 lands. /// /// Every screen is attempted even if an earlier one times out, so one slow /// screen cannot silently drop the rest; the skip is reported at the end. func testAllScreensAtLargestAccessibilitySize() throws { let app = AccessibilityAuditHarness.launch( contentSizeCategory: "UICTContentSizeCategoryAccessibilityXXXL" ) var unaudited: [String] = [] for tab in ["Inspect", "Dashboard", "Audit", "History", "Settings"] { app.selectRootTab(tab) let screen = "\(tab.lowercased())-accessibilityXXXL" if try !AccessibilityAuditHarness.audit(app, screen: screen, test: self) { unaudited.append(tab) } } try XCTSkipUnless( unaudited.isEmpty, "Audit did not complete in time for: \(unaudited.joined(separator: ", "))" ) } // MARK: Seeded audits — dense rows that never render on an empty simulator /// Dashboard with a populated portfolio: summary tiles, quick filters, /// activity/attention/expiry rows, and the grouped portfolio list. func testSeededDashboard() throws { let app = AccessibilityAuditHarness.launch(seeded: true) app.selectRootTab("Dashboard") let audited = try AccessibilityAuditHarness.audit(app, screen: "seeded-dashboard", test: self, reportOnly: true) try XCTSkipUnless(audited, "Audit did not complete in time for seeded Dashboard") } /// The watchlist's dense rows (up to nine text elements each). func testSeededTrackedDomains() throws { let app = AccessibilityAuditHarness.launch(seeded: true) app.selectRootTab("Settings") let trackedDomains = app.buttons["Tracked Domains"] XCTAssertTrue(trackedDomains.waitForExistence(timeout: 5)) trackedDomains.tap() let audited = try AccessibilityAuditHarness.audit(app, screen: "seeded-tracked-domains", test: self, reportOnly: true) try XCTSkipUnless(audited, "Audit did not complete in time for seeded Tracked Domains") } /// Batch result rows on the Inspect tab, including a failed lookup. func testSeededBatchResults() throws { let app = AccessibilityAuditHarness.launch(seeded: true) app.selectRootTab("Inspect") let audited = try AccessibilityAuditHarness.audit(app, screen: "seeded-batch", test: self, reportOnly: true) try XCTSkipUnless(audited, "Audit did not complete in time for seeded batch results") } /// The seeded screens again at the largest accessibility size — the case the /// deferred ViewThatFits work exists for. func testSeededScreensAtLargestAccessibilitySize() throws { let app = AccessibilityAuditHarness.launch( contentSizeCategory: "UICTContentSizeCategoryAccessibilityXXXL", seeded: true ) var unaudited: [String] = [] app.selectRootTab("Dashboard") if try !AccessibilityAuditHarness.audit(app, screen: "seeded-dashboard-accessibilityXXXL", test: self, reportOnly: true) { unaudited.append("Dashboard") } app.selectRootTab("Inspect") if try !AccessibilityAuditHarness.audit(app, screen: "seeded-batch-accessibilityXXXL", test: self, reportOnly: true) { unaudited.append("Inspect batch") } app.selectRootTab("Settings") let trackedDomains = app.buttons["Tracked Domains"] if trackedDomains.waitForExistence(timeout: 5) { trackedDomains.tap() if try !AccessibilityAuditHarness.audit(app, screen: "seeded-tracked-domains-accessibilityXXXL", test: self, reportOnly: true) { unaudited.append("Tracked Domains") } } try XCTSkipUnless( unaudited.isEmpty, "Audit did not complete in time for: \(unaudited.joined(separator: ", "))" ) } // MARK: Helpers private func auditRootTab(_ tab: String) throws { let app = AccessibilityAuditHarness.launch() app.selectRootTab(tab) let audited = try AccessibilityAuditHarness.audit(app, screen: tab.lowercased(), test: self) try XCTSkipUnless(audited, "Audit did not complete in time for \(tab)") } }