From 7f917e98b929dcf0f7901d0bc4eb05e04db3aa0c Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Wed, 22 Jul 2026 00:47:12 -0500 Subject: feat(a11y): seeded audit fixtures; fix dense-row reflow they exposed (#21) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dense rows and portfolio sections never rendered in the audit — the test simulator has no tracked domains or batch results — so five phases of row treatment shipped unmeasured. Driving the add-domain UI was tried earlier and rejected (keyboard contamination, persistent state), so this adds DOMAIN_DIG_SEED_FIXTURES: DEBUG-only launch argument, same pattern as DOMAIN_DIG_FORCE_PRO_PLUS, seeding four tracked domains and four batch results chosen to exercise every badge path, including a failed lookup and a stress-length domain name. Fixtures are strictly in-memory. persistTrackedDomains, refreshWidgetData (App Group file), refreshPersistedData, and refreshMonitoringState are all guarded while fixtures are active — the last one mattered: it runs right after seeding in the app task and was reloading the empty disk over the fixtures, which initially made the seeded watchlist audit pass by silently auditing the empty state. Four new audit tests cover the seeded Dashboard, Tracked Domains, and batch results at default and AccessibilityXXXL. What they found was real. At XXXL the watchlist row rendered the domain as "hea lt…" while the Registered badge wrapped one character per line into a screen-height capsule. Fixes, verified by before/after screenshots and the XXXL audits dropping to 7-8 findings per screen: - AppStatusBadgeView gets .fixedSize() — a capsule badge must never letter-wrap; taking natural width instead forces the row layout to its stacked alternative. - WatchlistRowView, BatchResultRowView, and PortfolioExpiryRow headers use ViewThatFits: domain-beside-badge while it genuinely fits, badge below the domain at accessibility sizes. Domain titles get fixedSize(horizontal: false, vertical: true) so they wrap rather than report a single-line ideal width to ViewThatFits and truncate. - The watchlist monitoring metadata strip (three texts abreast) stacks vertically when it no longer fits instead of wrapping mid-word. Known and deliberate: the seeded default-size audits still report a contrast/dynamicType wave attributed to "unknown element". Bisecting the row and badge accessibility modifiers showed most of it is an audit artifact on children-ignored content (the same rows measure 6-7:1 and render correctly); the artifact classes get characterised suppressions when enforcement lands, not blanket ones. --- DomainDigUITests/AccessibilityAuditTests.swift | 65 ++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) (limited to 'DomainDigUITests/AccessibilityAuditTests.swift') diff --git a/DomainDigUITests/AccessibilityAuditTests.swift b/DomainDigUITests/AccessibilityAuditTests.swift index 6a90bac..0863d32 100644 --- a/DomainDigUITests/AccessibilityAuditTests.swift +++ b/DomainDigUITests/AccessibilityAuditTests.swift @@ -82,6 +82,71 @@ final class AccessibilityAuditTests: XCTestCase { ) } + // 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) + 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) + 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) + 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) { + unaudited.append("Dashboard") + } + + app.selectRootTab("Inspect") + if try !AccessibilityAuditHarness.audit(app, screen: "seeded-batch-accessibilityXXXL", test: self) { + 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) { + 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 { -- cgit v1.2.3