From 38723030eb0db58a9d62a45c7725861901309a05 Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Mon, 20 Jul 2026 17:21:03 -0500 Subject: feat(a11y): add accessibility audit harness (#21 phase 0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 0 of the accessibility pass: a regression guard that must exist before any of the remedial phases, so their acceptance criteria are enforced rather than asserted once by hand. - Fix the project-level IPHONEOS_DEPLOYMENT_TARGET, which was 26.2 while all three targets are 17.6. It was shadowed everywhere today, but any target added later would silently inherit it and drop iOS 17.6 support with no error. - Add a DomainDigUITests target running performAccessibilityAudit on the six primary screens, plus a sweep of every root screen at AccessibilityXXXL. Uses the existing DOMAIN_DIG_FORCE_PRO_PLUS debug argument so Pro-gated screens are reachable. - Findings are reported, not failed. The audit surfaces violations that exist today, so gating on them would block unrelated PRs until the whole pass lands. Enforcement is a committed constant, AccessibilityAuditHarness.enforcedAuditTypes, widened per audit type as each phase clears a category. - CI now runs xcodebuild test across two simulators. Audit coverage is not nested between OS versions: on Tracked Domains, iOS 18.6 reported 2 findings and iOS 27.0 reported 6 (including contrast and element-detection issues 18.6 never raised), while at accessibility text sizes the Dashboard produced a hit-region finding on 18.6 that 27.0 did not. - Simulator selection is now dynamic and floor-aware. The previous selector took the first iPhone from any runtime, which can resolve to a simulator below the deployment target where the app cannot install. Baseline on iOS 18.6: 15 findings across 7 tests — text clipping on every screen, contrast on Inspect and Settings, and a hit-region failure on the Dashboard at accessibility text sizes. --- .github/workflows/build.yml | 102 +++++++++++++---- DomainDig.xcodeproj/project.pbxproj | 120 +++++++++++++++++++- .../xcshareddata/xcschemes/DomainDig.xcscheme | 12 ++ DomainDigUITests/AccessibilityAuditHarness.swift | 121 +++++++++++++++++++++ DomainDigUITests/AccessibilityAuditTests.swift | 84 ++++++++++++++ 5 files changed, 415 insertions(+), 24 deletions(-) create mode 100644 DomainDigUITests/AccessibilityAuditHarness.swift create mode 100644 DomainDigUITests/AccessibilityAuditTests.swift diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f4e7a2d..a6a2113 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,13 +1,37 @@ name: Build -# Compile gate for the GitHub mirror. builds.sr.ht is the primary remote for -# this project but has no macOS images, so xcodebuild cannot run there; this job -# compiles the app on a GitHub-hosted macOS runner instead. +# Compile gate and accessibility audit for the GitHub mirror. builds.sr.ht is the +# primary remote for this project but has no macOS images, so xcodebuild cannot +# run there; this job builds and tests on a GitHub-hosted macOS runner instead. # -# This runs `xcodebuild build`, not `test`: the project has no test target yet -# (planned for v5.0.0 in RELEASE_ROADMAP.md). When a test target and test plan -# exist, switch the final step to `xcodebuild test -testPlan ` and rename -# this workflow — the rest of the setup already matches a test run. +# This runs `xcodebuild test`, which also compiles the app, the widget, and the +# share extension (the DomainDig scheme's build action pulls both in as +# dependencies). The test target is DomainDigUITests — an accessibility audit +# suite; see DomainDigUITests/AccessibilityAuditHarness.swift. +# +# The audit REPORTS but does not FAIL by default. It surfaces violations that +# exist today, so gating on it would block every unrelated PR until the +# accessibility pass in issue #21 completes. Findings land in the job log and in +# the uploaded .xcresult bundle, tagged [report] or [FAIL]. +# +# Enforcement is a committed constant, not a CI setting: widen +# `AccessibilityAuditHarness.enforcedAuditTypes` as each phase clears a category. +# (Env vars were tried first — neither a plain xcodebuild env var nor a +# TEST_RUNNER_-prefixed build setting reaches the UI test process.) +# +# The matrix runs two simulators because audit coverage is NOT nested — each +# runtime reports findings the other misses, in both directions. Measured on the +# Tracked Domains screen, iOS 18.6 reported 2 issues and iOS 27.0 reported 6 +# (including contrast and element-detection issues 18.6 never raised); at +# accessibility text sizes the Dashboard produced a hit-region finding on 18.6 +# that 27.0 did not. Testing only the newest image would leave the oldest +# supported OS unchecked; testing only the floor would miss newer audit checks. +# +# Runtimes are resolved dynamically rather than pinned: the deployment target is +# 17.6, but no 17.6 simulator runtime ships, so "floor" means the oldest +# available runtime at or above the deployment target (18.6 at time of writing). +# The previous selector took the first iPhone from any runtime, which could pick +# a simulator BELOW the deployment target, where the app cannot install. # # pull_request only, plus manual dispatch. GitHub builds the merge result (PR # merged into main), so a green PR validates exactly what will land on main. @@ -31,13 +55,25 @@ concurrency: group: build-${{ github.ref }} cancel-in-progress: true +env: + # Keep in sync with IPHONEOS_DEPLOYMENT_TARGET in DomainDig.xcodeproj. + DEPLOYMENT_TARGET_MAJOR: '17' + DEPLOYMENT_TARGET_MINOR: '6' + jobs: - build: - name: xcodebuild build - # macos-latest still points at macOS 15, which lacks the iOS 26 SDK this app - # targets (deployment target 26.2). + test: + name: xcodebuild test (${{ matrix.tier }}) + # macos-latest still points at macOS 15, which lacks the iOS 26+ SDK this app + # is built against. runs-on: macos-26 + strategy: + fail-fast: false + matrix: + # floor = oldest runtime the app actually supports + # current = newest runtime available on the image + tier: [floor, current] + steps: - uses: actions/checkout@v7 @@ -50,29 +86,51 @@ jobs: id: sim run: | set -euo pipefail - udid=$(xcrun simctl list devices available --json \ - | jq -r '[.devices[][] | select(.name | startswith("iPhone"))] | first | .udid') - if [ -z "$udid" ] || [ "$udid" = "null" ]; then - echo "No available iPhone simulator on this image" >&2 + floor=$(( DEPLOYMENT_TARGET_MAJOR * 1000 + DEPLOYMENT_TARGET_MINOR )) + + selected=$(xcrun simctl list devices available --json \ + | jq -c --argjson floor "$floor" --arg tier "${{ matrix.tier }}" ' + [ .devices | to_entries[] + | (.key | capture("SimRuntime\\.iOS-(?[0-9]+)-(?[0-9]+)$")) as $v + | (($v.maj | tonumber) * 1000 + ($v.min | tonumber)) as $rank + | select($rank >= $floor) + | .value[] + | select(.name | startswith("iPhone")) + | { rank: $rank, udid: .udid, name: .name, os: "\($v.maj).\($v.min)" } + ] + | sort_by(.rank, .name) + | if length == 0 then empty + elif $tier == "floor" then .[0] + else .[-1] end + ') + + if [ -z "$selected" ]; then + echo "::error::No iPhone simulator at or above iOS ${DEPLOYMENT_TARGET_MAJOR}.${DEPLOYMENT_TARGET_MINOR} on this image" xcrun simctl list devices available >&2 exit 1 fi - echo "udid=$udid" >> "$GITHUB_OUTPUT" - - name: Build + label=$(echo "$selected" | jq -r '"\(.name) (iOS \(.os))"') + echo "Selected $label" + echo "udid=$(echo "$selected" | jq -r .udid)" >> "$GITHUB_OUTPUT" + echo "label=$label" >> "$GITHUB_OUTPUT" + + - name: Test on ${{ steps.sim.outputs.label }} run: | set -o pipefail - xcodebuild build \ + xcodebuild test \ -project DomainDig.xcodeproj \ -scheme DomainDig \ -destination "id=${{ steps.sim.outputs.udid }}" \ - -resultBundlePath BuildResults.xcresult \ + -resultBundlePath TestResults.xcresult \ CODE_SIGNING_ALLOWED=NO - name: Upload results - if: failure() + # Always upload: on success the bundle carries the accessibility burndown + # list, which is the reason this suite exists. + if: always() uses: actions/upload-artifact@v4 with: - name: build-results - path: BuildResults.xcresult + name: test-results-${{ matrix.tier }} + path: TestResults.xcresult retention-days: 7 diff --git a/DomainDig.xcodeproj/project.pbxproj b/DomainDig.xcodeproj/project.pbxproj index 8889f33..6b4c25c 100644 --- a/DomainDig.xcodeproj/project.pbxproj +++ b/DomainDig.xcodeproj/project.pbxproj @@ -33,6 +33,13 @@ remoteGlobalIDString = 8BDB00010000000000000006; remoteInfo = DomainDigShareExtension; }; + 8BDC00010000000000000003 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 8B7800612F6090E300933221 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 8B7800682F6090E300933221; + remoteInfo = DomainDig; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -61,6 +68,7 @@ 8BCA3CBD2F9C8D57004B742C /* LocalAPIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAPIService.swift; sourceTree = ""; }; 8BDA00010000000000000001 /* DomainDigWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = DomainDigWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 8BDB00010000000000000001 /* DomainDigShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = DomainDigShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 8BDC00010000000000000001 /* DomainDigUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DomainDigUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 8BF9DA842F9B13FB00EF41D5 /* DomainDataPortabilityService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainDataPortabilityService.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -120,6 +128,11 @@ path = DomainDigShareExtension; sourceTree = ""; }; + 8BDC0001000000000000000E /* DomainDigUITests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = DomainDigUITests; + sourceTree = ""; + }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -144,6 +157,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 8BDC00010000000000000008 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -155,6 +175,7 @@ 8BDA0001000000000000000D /* Shared */, 8BDA0001000000000000000E /* DomainDigWidget */, 8BDB0001000000000000000E /* DomainDigShareExtension */, + 8BDC0001000000000000000E /* DomainDigUITests */, 8B78006A2F6090E300933221 /* Products */, 8BBFEF062F9874AE00E8E144 /* LookupSnapshot.swift */, 8BBFEF042F9874AE00E8E144 /* DomainReportBuilder.swift */, @@ -172,6 +193,7 @@ 8B7800692F6090E300933221 /* DomainDig.app */, 8BDA00010000000000000001 /* DomainDigWidgetExtension.appex */, 8BDB00010000000000000001 /* DomainDigShareExtension.appex */, + 8BDC00010000000000000001 /* DomainDigUITests.xctest */, ); name = Products; sourceTree = ""; @@ -251,6 +273,29 @@ productReference = 8BDB00010000000000000001 /* DomainDigShareExtension.appex */; productType = "com.apple.product-type.app-extension"; }; + 8BDC00010000000000000006 /* DomainDigUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 8BDC0001000000000000000A /* Build configuration list for PBXNativeTarget "DomainDigUITests" */; + buildPhases = ( + 8BDC00010000000000000007 /* Sources */, + 8BDC00010000000000000008 /* Frameworks */, + 8BDC00010000000000000009 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 8BDC00010000000000000004 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 8BDC0001000000000000000E /* DomainDigUITests */, + ); + name = DomainDigUITests; + packageProductDependencies = ( + ); + productName = DomainDigUITests; + productReference = 8BDC00010000000000000001 /* DomainDigUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -275,6 +320,10 @@ 8BDB00010000000000000006 = { CreatedOnToolsVersion = 26.3; }; + 8BDC00010000000000000006 = { + CreatedOnToolsVersion = 27.0; + TestTargetID = 8B7800682F6090E300933221; + }; }; }; buildConfigurationList = 8B7800642F6090E300933221 /* Build configuration list for PBXProject "DomainDig" */; @@ -294,6 +343,7 @@ 8B7800682F6090E300933221 /* DomainDig */, 8BDA00010000000000000006 /* DomainDigWidgetExtension */, 8BDB00010000000000000006 /* DomainDigShareExtension */, + 8BDC00010000000000000006 /* DomainDigUITests */, ); }; /* End PBXProject section */ @@ -320,6 +370,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 8BDC00010000000000000009 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -351,6 +408,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 8BDC00010000000000000007 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -364,6 +428,11 @@ target = 8BDB00010000000000000006 /* DomainDigShareExtension */; targetProxy = 8BDB00010000000000000003 /* PBXContainerItemProxy */; }; + 8BDC00010000000000000004 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 8B7800682F6090E300933221 /* DomainDig */; + targetProxy = 8BDC00010000000000000003 /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ @@ -420,7 +489,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 26.2; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -479,7 +548,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 26.2; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; @@ -680,6 +749,44 @@ }; name = Release; }; + 8BDC0001000000000000000B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 43; + DEVELOPMENT_TEAM = ZCNAX3VL9D; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; + MARKETING_VERSION = 4.8.3; + PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDigUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = DomainDig; + }; + name = Debug; + }; + 8BDC0001000000000000000C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 43; + DEVELOPMENT_TEAM = ZCNAX3VL9D; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; + MARKETING_VERSION = 4.8.3; + PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDigUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = DomainDig; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -719,6 +826,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 8BDC0001000000000000000A /* Build configuration list for PBXNativeTarget "DomainDigUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8BDC0001000000000000000B /* Debug */, + 8BDC0001000000000000000C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ }; rootObject = 8B7800612F6090E300933221 /* Project object */; diff --git a/DomainDig.xcodeproj/xcshareddata/xcschemes/DomainDig.xcscheme b/DomainDig.xcodeproj/xcshareddata/xcschemes/DomainDig.xcscheme index 8f733d7..5b0bc8e 100644 --- a/DomainDig.xcodeproj/xcshareddata/xcschemes/DomainDig.xcscheme +++ b/DomainDig.xcodeproj/xcshareddata/xcschemes/DomainDig.xcscheme @@ -29,6 +29,18 @@ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES" shouldAutocreateTestPlan = "YES"> + + + + + + XCUIApplication { + let app = XCUIApplication() + app.launchArguments = [forceProPlusArgument] + if let contentSizeCategory { + app.launchArguments += ["-UIPreferredContentSizeCategoryName", contentSizeCategory] + } + app.launch() + return app + } + + /// Runs a full audit and records every finding against the test. + /// + /// Findings are logged and attached to the result bundle so a CI run + /// produces the burndown list as an artifact rather than only a pass/fail. + static func audit( + _ app: XCUIApplication, + screen: String, + test: XCTestCase + ) throws { + var findings: [String] = [] + + try app.performAccessibilityAudit { issue in + let isEnforced = !enforcedAuditTypes.intersection(issue.auditType).isEmpty + let marker = isEnforced ? "FAIL" : "report" + findings.append("[\(marker)][\(name(for: issue.auditType))] \(issue.compactDescription)") + // true suppresses the finding, false reports it as a test failure. + return !isEnforced + } + + let summary = findings.isEmpty + ? "\(screen): no accessibility findings" + : "\(screen): \(findings.count) finding(s)\n" + findings.sorted().map { " • \($0)" }.joined(separator: "\n") + + print(summary) + + let attachment = XCTAttachment(string: summary) + attachment.name = "a11y-audit-\(screen)" + attachment.lifetime = .keepAlways + test.add(attachment) + } + + /// `XCUIAccessibilityAuditType` is an option set whose description is just a + /// raw bitmask, which makes the burndown list unreadable. Resolve it against + /// the named members rather than hard-coding bit positions, so this keeps + /// working if Apple adds audit types. + private static func name(for type: XCUIAccessibilityAuditType) -> String { + let known: [(XCUIAccessibilityAuditType, String)] = [ + (.contrast, "contrast"), + (.elementDetection, "elementDetection"), + (.hitRegion, "hitRegion"), + (.sufficientElementDescription, "sufficientElementDescription"), + (.dynamicType, "dynamicType"), + (.textClipped, "textClipped"), + (.trait, "trait") + ] + let matched = known.filter { type.contains($0.0) }.map(\.1) + return matched.isEmpty ? "unknown(\(type.rawValue))" : matched.joined(separator: "+") + } +} + +extension XCUIApplication { + /// Taps a root tab by its visible label. + /// + /// Falls back to a plain button query because the tab bar is only present in + /// the compact size class — in regular width `RootTabView` renders a + /// `NavigationSplitView` sidebar instead. + @MainActor + func selectRootTab(_ name: String) { + let tabButton = tabBars.buttons[name] + let element = tabButton.waitForExistence(timeout: 5) ? tabButton : buttons[name] + XCTAssertTrue( + element.waitForExistence(timeout: 5), + "Could not find a way to reach the \(name) screen" + ) + element.tap() + } +} diff --git a/DomainDigUITests/AccessibilityAuditTests.swift b/DomainDigUITests/AccessibilityAuditTests.swift new file mode 100644 index 0000000..63b3d02 --- /dev/null +++ b/DomainDigUITests/AccessibilityAuditTests.swift @@ -0,0 +1,84 @@ +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 { + let app = AccessibilityAuditHarness.launch() + app.selectRootTab("Inspect") + try AccessibilityAuditHarness.audit(app, screen: "inspect", test: self) + } + + func testDashboardScreen() throws { + let app = AccessibilityAuditHarness.launch() + app.selectRootTab("Dashboard") + try AccessibilityAuditHarness.audit(app, screen: "dashboard", test: self) + } + + func testAuditScreen() throws { + let app = AccessibilityAuditHarness.launch() + app.selectRootTab("Audit") + try AccessibilityAuditHarness.audit(app, screen: "audit", test: self) + } + + func testHistoryScreen() throws { + let app = AccessibilityAuditHarness.launch() + app.selectRootTab("History") + try AccessibilityAuditHarness.audit(app, screen: "history", test: self) + } + + func testSettingsScreen() throws { + let app = AccessibilityAuditHarness.launch() + app.selectRootTab("Settings") + try AccessibilityAuditHarness.audit(app, screen: "settings", test: self) + } + + 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() + + try AccessibilityAuditHarness.audit(app, screen: "tracked-domains", test: self) + } + + // 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. + func testAllScreensAtLargestAccessibilitySize() throws { + let app = AccessibilityAuditHarness.launch( + contentSizeCategory: "UICTContentSizeCategoryAccessibilityXXXL" + ) + + for tab in ["Inspect", "Dashboard", "Audit", "History", "Settings"] { + app.selectRootTab(tab) + try AccessibilityAuditHarness.audit( + app, + screen: "\(tab.lowercased())-accessibilityXXXL", + test: self + ) + } + } +} -- cgit v1.2.3