diff options
| -rw-r--r-- | .github/workflows/build.yml | 102 | ||||
| -rw-r--r-- | DomainDig.xcodeproj/project.pbxproj | 120 | ||||
| -rw-r--r-- | DomainDig.xcodeproj/xcshareddata/xcschemes/DomainDig.xcscheme | 12 | ||||
| -rw-r--r-- | DomainDigUITests/AccessibilityAuditHarness.swift | 121 | ||||
| -rw-r--r-- | DomainDigUITests/AccessibilityAuditTests.swift | 84 |
5 files changed, 415 insertions, 24 deletions
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 <name>` 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-(?<maj>[0-9]+)-(?<min>[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 = "<group>"; }; 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 = "<group>"; }; /* End PBXFileReference section */ @@ -120,6 +128,11 @@ path = DomainDigShareExtension; sourceTree = "<group>"; }; + 8BDC0001000000000000000E /* DomainDigUITests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = DomainDigUITests; + sourceTree = "<group>"; + }; /* 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 = "<group>"; @@ -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"> + <Testables> + <TestableReference + skipped = "NO"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "8BDC00010000000000000006" + BuildableName = "DomainDigUITests.xctest" + BlueprintName = "DomainDigUITests" + ReferencedContainer = "container:DomainDig.xcodeproj"> + </BuildableReference> + </TestableReference> + </Testables> </TestAction> <LaunchAction buildConfiguration = "Debug" diff --git a/DomainDigUITests/AccessibilityAuditHarness.swift b/DomainDigUITests/AccessibilityAuditHarness.swift new file mode 100644 index 0000000..80257e1 --- /dev/null +++ b/DomainDigUITests/AccessibilityAuditHarness.swift @@ -0,0 +1,121 @@ +import XCTest + +/// Shared plumbing for the accessibility audit suite. +/// +/// `performAccessibilityAudit` checks contrast, hit-region size, clipped text at +/// large Dynamic Type, element descriptions, and trait correctness — the same +/// categories the accessibility pass in issue #21 works through. +/// +/// **The suite reports by default and fails only for enforced categories.** The +/// audit surfaces violations that exist today, so failing on everything would +/// block unrelated PRs until the whole pass lands. `enforcedAuditTypes` below is +/// the ratchet: widen it as each phase of #21 clears a category. +/// +/// Two alternatives were tried and rejected: +/// +/// - *A per-screen baseline count.* Audit coverage is not nested across OS +/// versions — the same screen legitimately yields different counts on the +/// floor simulator and the current one, so no single committed number is +/// correct for both. +/// - *An environment variable.* Neither a plain `xcodebuild` env var nor a +/// `TEST_RUNNER_`-prefixed build setting reaches this process, so the toggle +/// silently did nothing. A committed constant also makes "when did contrast +/// become enforced?" answerable with `git blame` instead of CI tribal +/// knowledge. +@MainActor +enum AccessibilityAuditHarness { + /// Launch argument that lifts feature gating so Pro-only screens are + /// reachable. `PurchaseService` honours this in `DEBUG` builds only. + private static let forceProPlusArgument = "DOMAIN_DIG_FORCE_PRO_PLUS" + + /// Audit categories that fail the build. Everything else is reported only. + /// + /// Empty until the accessibility pass starts landing. Suggested ratchet, + /// following the phases in issue #21: + /// + /// - after phase 2 (semantic colors + light mode): `.contrast` + /// - after phase 3 (Dynamic Type + reflow): `.textClipped`, `.dynamicType`, + /// `.hitRegion` + /// - after phase 4 (VoiceOver): `.elementDetection`, + /// `.sufficientElementDescription`, `.trait` + static let enforcedAuditTypes: XCUIAccessibilityAuditType = [] + + /// Launches the app with feature gating lifted, optionally at a specific + /// content size category. + static func launch(contentSizeCategory: String? = nil) -> 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 + ) + } + } +} |
