Commit 38723030eb
Unsigned
Layout: unified · split
.github/workflows/build.yml +80 −22
| @@ -1,13 +1,37 @@ | |||
| 1 | name: Build | 1 | name: Build |
| 2 | 2 | ||
| 3 | # Compile gate for the GitHub mirror. builds.sr.ht is the primary remote for | 3 | # Compile gate and accessibility audit for the GitHub mirror. builds.sr.ht is the |
| 4 | # this project but has no macOS images, so xcodebuild cannot run there; this job | 4 | # primary remote for this project but has no macOS images, so xcodebuild cannot |
| 5 | # compiles the app on a GitHub-hosted macOS runner instead. | 5 | # run there; this job builds and tests on a GitHub-hosted macOS runner instead. |
| 6 | # | 6 | # |
| 7 | # This runs `xcodebuild build`, not `test`: the project has no test target yet | 7 | # This runs `xcodebuild test`, which also compiles the app, the widget, and the |
| 8 | # (planned for v5.0.0 in RELEASE_ROADMAP.md). When a test target and test plan | 8 | # share extension (the DomainDig scheme's build action pulls both in as |
| 9 | # exist, switch the final step to `xcodebuild test -testPlan <name>` and rename | 9 | # dependencies). The test target is DomainDigUITests — an accessibility audit |
| 10 | # this workflow — the rest of the setup already matches a test run. | 10 | # suite; see DomainDigUITests/AccessibilityAuditHarness.swift. |
| 11 | # | ||
| 12 | # The audit REPORTS but does not FAIL by default. It surfaces violations that | ||
| 13 | # exist today, so gating on it would block every unrelated PR until the | ||
| 14 | # accessibility pass in issue #21 completes. Findings land in the job log and in | ||
| 15 | # the uploaded .xcresult bundle, tagged [report] or [FAIL]. | ||
| 16 | # | ||
| 17 | # Enforcement is a committed constant, not a CI setting: widen | ||
| 18 | # `AccessibilityAuditHarness.enforcedAuditTypes` as each phase clears a category. | ||
| 19 | # (Env vars were tried first — neither a plain xcodebuild env var nor a | ||
| 20 | # TEST_RUNNER_-prefixed build setting reaches the UI test process.) | ||
| 21 | # | ||
| 22 | # The matrix runs two simulators because audit coverage is NOT nested — each | ||
| 23 | # runtime reports findings the other misses, in both directions. Measured on the | ||
| 24 | # Tracked Domains screen, iOS 18.6 reported 2 issues and iOS 27.0 reported 6 | ||
| 25 | # (including contrast and element-detection issues 18.6 never raised); at | ||
| 26 | # accessibility text sizes the Dashboard produced a hit-region finding on 18.6 | ||
| 27 | # that 27.0 did not. Testing only the newest image would leave the oldest | ||
| 28 | # supported OS unchecked; testing only the floor would miss newer audit checks. | ||
| 29 | # | ||
| 30 | # Runtimes are resolved dynamically rather than pinned: the deployment target is | ||
| 31 | # 17.6, but no 17.6 simulator runtime ships, so "floor" means the oldest | ||
| 32 | # available runtime at or above the deployment target (18.6 at time of writing). | ||
| 33 | # The previous selector took the first iPhone from any runtime, which could pick | ||
| 34 | # a simulator BELOW the deployment target, where the app cannot install. | ||
| 11 | # | 35 | # |
| 12 | # pull_request only, plus manual dispatch. GitHub builds the merge result (PR | 36 | # pull_request only, plus manual dispatch. GitHub builds the merge result (PR |
| 13 | # merged into main), so a green PR validates exactly what will land on main. | 37 | # merged into main), so a green PR validates exactly what will land on main. |
| @@ -31,13 +55,25 @@ concurrency: | |||
| 31 | group: build-${{ github.ref }} | 55 | group: build-${{ github.ref }} |
| 32 | cancel-in-progress: true | 56 | cancel-in-progress: true |
| 33 | 57 | ||
| 58 | env: | ||
| 59 | # Keep in sync with IPHONEOS_DEPLOYMENT_TARGET in DomainDig.xcodeproj. | ||
| 60 | DEPLOYMENT_TARGET_MAJOR: '17' | ||
| 61 | DEPLOYMENT_TARGET_MINOR: '6' | ||
| 62 | |||
| 34 | jobs: | 63 | jobs: |
| 35 | build: | 64 | test: |
| 36 | name: xcodebuild build | 65 | name: xcodebuild test (${{ matrix.tier }}) |
| 37 | # macos-latest still points at macOS 15, which lacks the iOS 26 SDK this app | 66 | # macos-latest still points at macOS 15, which lacks the iOS 26+ SDK this app |
| 38 | # targets (deployment target 26.2). | 67 | # is built against. |
| 39 | runs-on: macos-26 | 68 | runs-on: macos-26 |
| 40 | 69 | ||
| 70 | strategy: | ||
| 71 | fail-fast: false | ||
| 72 | matrix: | ||
| 73 | # floor = oldest runtime the app actually supports | ||
| 74 | # current = newest runtime available on the image | ||
| 75 | tier: [floor, current] | ||
| 76 | |||
| 41 | steps: | 77 | steps: |
| 42 | - uses: actions/checkout@v7 | 78 | - uses: actions/checkout@v7 |
| 43 | 79 | ||
| @@ -50,29 +86,51 @@ jobs: | |||
| 50 | id: sim | 86 | id: sim |
| 51 | run: | | 87 | run: | |
| 52 | set -euo pipefail | 88 | set -euo pipefail |
| 53 | udid=$(xcrun simctl list devices available --json \ | 89 | floor=$(( DEPLOYMENT_TARGET_MAJOR * 1000 + DEPLOYMENT_TARGET_MINOR )) |
| 54 | | jq -r '[.devices[][] | select(.name | startswith("iPhone"))] | first | .udid') | 90 | |
| 55 | if [ -z "$udid" ] || [ "$udid" = "null" ]; then | 91 | selected=$(xcrun simctl list devices available --json \ |
| 56 | echo "No available iPhone simulator on this image" >&2 | 92 | | jq -c --argjson floor "$floor" --arg tier "${{ matrix.tier }}" ' |
| 93 | [ .devices | to_entries[] | ||
| 94 | | (.key | capture("SimRuntime\\.iOS-(?<maj>[0-9]+)-(?<min>[0-9]+)$")) as $v | ||
| 95 | | (($v.maj | tonumber) * 1000 + ($v.min | tonumber)) as $rank | ||
| 96 | | select($rank >= $floor) | ||
| 97 | | .value[] | ||
| 98 | | select(.name | startswith("iPhone")) | ||
| 99 | | { rank: $rank, udid: .udid, name: .name, os: "\($v.maj).\($v.min)" } | ||
| 100 | ] | ||
| 101 | | sort_by(.rank, .name) | ||
| 102 | | if length == 0 then empty | ||
| 103 | elif $tier == "floor" then .[0] | ||
| 104 | else .[-1] end | ||
| 105 | ') | ||
| 106 | |||
| 107 | if [ -z "$selected" ]; then | ||
| 108 | echo "::error::No iPhone simulator at or above iOS ${DEPLOYMENT_TARGET_MAJOR}.${DEPLOYMENT_TARGET_MINOR} on this image" | ||
| 57 | xcrun simctl list devices available >&2 | 109 | xcrun simctl list devices available >&2 |
| 58 | exit 1 | 110 | exit 1 |
| 59 | fi | 111 | fi |
| 60 | echo "udid=$udid" >> "$GITHUB_OUTPUT" | ||
| 61 | 112 | ||
| 62 | - name: Build | 113 | label=$(echo "$selected" | jq -r '"\(.name) (iOS \(.os))"') |
| 114 | echo "Selected $label" | ||
| 115 | echo "udid=$(echo "$selected" | jq -r .udid)" >> "$GITHUB_OUTPUT" | ||
| 116 | echo "label=$label" >> "$GITHUB_OUTPUT" | ||
| 117 | |||
| 118 | - name: Test on ${{ steps.sim.outputs.label }} | ||
| 63 | run: | | 119 | run: | |
| 64 | set -o pipefail | 120 | set -o pipefail |
| 65 | xcodebuild build \ | 121 | xcodebuild test \ |
| 66 | -project DomainDig.xcodeproj \ | 122 | -project DomainDig.xcodeproj \ |
| 67 | -scheme DomainDig \ | 123 | -scheme DomainDig \ |
| 68 | -destination "id=${{ steps.sim.outputs.udid }}" \ | 124 | -destination "id=${{ steps.sim.outputs.udid }}" \ |
| 69 | -resultBundlePath BuildResults.xcresult \ | 125 | -resultBundlePath TestResults.xcresult \ |
| 70 | CODE_SIGNING_ALLOWED=NO | 126 | CODE_SIGNING_ALLOWED=NO |
| 71 | 127 | ||
| 72 | - name: Upload results | 128 | - name: Upload results |
| 73 | if: failure() | 129 | # Always upload: on success the bundle carries the accessibility burndown |
| 130 | # list, which is the reason this suite exists. | ||
| 131 | if: always() | ||
| 74 | uses: actions/upload-artifact@v4 | 132 | uses: actions/upload-artifact@v4 |
| 75 | with: | 133 | with: |
| 76 | name: build-results | 134 | name: test-results-${{ matrix.tier }} |
| 77 | path: BuildResults.xcresult | 135 | path: TestResults.xcresult |
| 78 | retention-days: 7 | 136 | retention-days: 7 |
DomainDig.xcodeproj/project.pbxproj +118 −2
| @@ -33,6 +33,13 @@ | |||
| 33 | remoteGlobalIDString = 8BDB00010000000000000006; | 33 | remoteGlobalIDString = 8BDB00010000000000000006; |
| 34 | remoteInfo = DomainDigShareExtension; | 34 | remoteInfo = DomainDigShareExtension; |
| 35 | }; | 35 | }; |
| 36 | 8BDC00010000000000000003 /* PBXContainerItemProxy */ = { | ||
| 37 | isa = PBXContainerItemProxy; | ||
| 38 | containerPortal = 8B7800612F6090E300933221 /* Project object */; | ||
| 39 | proxyType = 1; | ||
| 40 | remoteGlobalIDString = 8B7800682F6090E300933221; | ||
| 41 | remoteInfo = DomainDig; | ||
| 42 | }; | ||
| 36 | /* End PBXContainerItemProxy section */ | 43 | /* End PBXContainerItemProxy section */ |
| 37 | 44 | ||
| 38 | /* Begin PBXCopyFilesBuildPhase section */ | 45 | /* Begin PBXCopyFilesBuildPhase section */ |
| @@ -61,6 +68,7 @@ | |||
| 61 | 8BCA3CBD2F9C8D57004B742C /* LocalAPIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAPIService.swift; sourceTree = "<group>"; }; | 68 | 8BCA3CBD2F9C8D57004B742C /* LocalAPIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAPIService.swift; sourceTree = "<group>"; }; |
| 62 | 8BDA00010000000000000001 /* DomainDigWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = DomainDigWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; | 69 | 8BDA00010000000000000001 /* DomainDigWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = DomainDigWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; |
| 63 | 8BDB00010000000000000001 /* DomainDigShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = DomainDigShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; | 70 | 8BDB00010000000000000001 /* DomainDigShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = DomainDigShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; |
| 71 | 8BDC00010000000000000001 /* DomainDigUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DomainDigUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; | ||
| 64 | 8BF9DA842F9B13FB00EF41D5 /* DomainDataPortabilityService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainDataPortabilityService.swift; sourceTree = "<group>"; }; | 72 | 8BF9DA842F9B13FB00EF41D5 /* DomainDataPortabilityService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainDataPortabilityService.swift; sourceTree = "<group>"; }; |
| 65 | /* End PBXFileReference section */ | 73 | /* End PBXFileReference section */ |
| 66 | 74 | ||
| @@ -120,6 +128,11 @@ | |||
| 120 | path = DomainDigShareExtension; | 128 | path = DomainDigShareExtension; |
| 121 | sourceTree = "<group>"; | 129 | sourceTree = "<group>"; |
| 122 | }; | 130 | }; |
| 131 | 8BDC0001000000000000000E /* DomainDigUITests */ = { | ||
| 132 | isa = PBXFileSystemSynchronizedRootGroup; | ||
| 133 | path = DomainDigUITests; | ||
| 134 | sourceTree = "<group>"; | ||
| 135 | }; | ||
| 123 | /* End PBXFileSystemSynchronizedRootGroup section */ | 136 | /* End PBXFileSystemSynchronizedRootGroup section */ |
| 124 | 137 | ||
| 125 | /* Begin PBXFrameworksBuildPhase section */ | 138 | /* Begin PBXFrameworksBuildPhase section */ |
| @@ -144,6 +157,13 @@ | |||
| 144 | ); | 157 | ); |
| 145 | runOnlyForDeploymentPostprocessing = 0; | 158 | runOnlyForDeploymentPostprocessing = 0; |
| 146 | }; | 159 | }; |
| 160 | 8BDC00010000000000000008 /* Frameworks */ = { | ||
| 161 | isa = PBXFrameworksBuildPhase; | ||
| 162 | buildActionMask = 2147483647; | ||
| 163 | files = ( | ||
| 164 | ); | ||
| 165 | runOnlyForDeploymentPostprocessing = 0; | ||
| 166 | }; | ||
| 147 | /* End PBXFrameworksBuildPhase section */ | 167 | /* End PBXFrameworksBuildPhase section */ |
| 148 | 168 | ||
| 149 | /* Begin PBXGroup section */ | 169 | /* Begin PBXGroup section */ |
| @@ -155,6 +175,7 @@ | |||
| 155 | 8BDA0001000000000000000D /* Shared */, | 175 | 8BDA0001000000000000000D /* Shared */, |
| 156 | 8BDA0001000000000000000E /* DomainDigWidget */, | 176 | 8BDA0001000000000000000E /* DomainDigWidget */, |
| 157 | 8BDB0001000000000000000E /* DomainDigShareExtension */, | 177 | 8BDB0001000000000000000E /* DomainDigShareExtension */, |
| 178 | 8BDC0001000000000000000E /* DomainDigUITests */, | ||
| 158 | 8B78006A2F6090E300933221 /* Products */, | 179 | 8B78006A2F6090E300933221 /* Products */, |
| 159 | 8BBFEF062F9874AE00E8E144 /* LookupSnapshot.swift */, | 180 | 8BBFEF062F9874AE00E8E144 /* LookupSnapshot.swift */, |
| 160 | 8BBFEF042F9874AE00E8E144 /* DomainReportBuilder.swift */, | 181 | 8BBFEF042F9874AE00E8E144 /* DomainReportBuilder.swift */, |
| @@ -172,6 +193,7 @@ | |||
| 172 | 8B7800692F6090E300933221 /* DomainDig.app */, | 193 | 8B7800692F6090E300933221 /* DomainDig.app */, |
| 173 | 8BDA00010000000000000001 /* DomainDigWidgetExtension.appex */, | 194 | 8BDA00010000000000000001 /* DomainDigWidgetExtension.appex */, |
| 174 | 8BDB00010000000000000001 /* DomainDigShareExtension.appex */, | 195 | 8BDB00010000000000000001 /* DomainDigShareExtension.appex */, |
| 196 | 8BDC00010000000000000001 /* DomainDigUITests.xctest */, | ||
| 175 | ); | 197 | ); |
| 176 | name = Products; | 198 | name = Products; |
| 177 | sourceTree = "<group>"; | 199 | sourceTree = "<group>"; |
| @@ -251,6 +273,29 @@ | |||
| 251 | productReference = 8BDB00010000000000000001 /* DomainDigShareExtension.appex */; | 273 | productReference = 8BDB00010000000000000001 /* DomainDigShareExtension.appex */; |
| 252 | productType = "com.apple.product-type.app-extension"; | 274 | productType = "com.apple.product-type.app-extension"; |
| 253 | }; | 275 | }; |
| 276 | 8BDC00010000000000000006 /* DomainDigUITests */ = { | ||
| 277 | isa = PBXNativeTarget; | ||
| 278 | buildConfigurationList = 8BDC0001000000000000000A /* Build configuration list for PBXNativeTarget "DomainDigUITests" */; | ||
| 279 | buildPhases = ( | ||
| 280 | 8BDC00010000000000000007 /* Sources */, | ||
| 281 | 8BDC00010000000000000008 /* Frameworks */, | ||
| 282 | 8BDC00010000000000000009 /* Resources */, | ||
| 283 | ); | ||
| 284 | buildRules = ( | ||
| 285 | ); | ||
| 286 | dependencies = ( | ||
| 287 | 8BDC00010000000000000004 /* PBXTargetDependency */, | ||
| 288 | ); | ||
| 289 | fileSystemSynchronizedGroups = ( | ||
| 290 | 8BDC0001000000000000000E /* DomainDigUITests */, | ||
| 291 | ); | ||
| 292 | name = DomainDigUITests; | ||
| 293 | packageProductDependencies = ( | ||
| 294 | ); | ||
| 295 | productName = DomainDigUITests; | ||
| 296 | productReference = 8BDC00010000000000000001 /* DomainDigUITests.xctest */; | ||
| 297 | productType = "com.apple.product-type.bundle.ui-testing"; | ||
| 298 | }; | ||
| 254 | /* End PBXNativeTarget section */ | 299 | /* End PBXNativeTarget section */ |
| 255 | 300 | ||
| 256 | /* Begin PBXProject section */ | 301 | /* Begin PBXProject section */ |
| @@ -275,6 +320,10 @@ | |||
| 275 | 8BDB00010000000000000006 = { | 320 | 8BDB00010000000000000006 = { |
| 276 | CreatedOnToolsVersion = 26.3; | 321 | CreatedOnToolsVersion = 26.3; |
| 277 | }; | 322 | }; |
| 323 | 8BDC00010000000000000006 = { | ||
| 324 | CreatedOnToolsVersion = 27.0; | ||
| 325 | TestTargetID = 8B7800682F6090E300933221; | ||
| 326 | }; | ||
| 278 | }; | 327 | }; |
| 279 | }; | 328 | }; |
| 280 | buildConfigurationList = 8B7800642F6090E300933221 /* Build configuration list for PBXProject "DomainDig" */; | 329 | buildConfigurationList = 8B7800642F6090E300933221 /* Build configuration list for PBXProject "DomainDig" */; |
| @@ -294,6 +343,7 @@ | |||
| 294 | 8B7800682F6090E300933221 /* DomainDig */, | 343 | 8B7800682F6090E300933221 /* DomainDig */, |
| 295 | 8BDA00010000000000000006 /* DomainDigWidgetExtension */, | 344 | 8BDA00010000000000000006 /* DomainDigWidgetExtension */, |
| 296 | 8BDB00010000000000000006 /* DomainDigShareExtension */, | 345 | 8BDB00010000000000000006 /* DomainDigShareExtension */, |
| 346 | 8BDC00010000000000000006 /* DomainDigUITests */, | ||
| 297 | ); | 347 | ); |
| 298 | }; | 348 | }; |
| 299 | /* End PBXProject section */ | 349 | /* End PBXProject section */ |
| @@ -320,6 +370,13 @@ | |||
| 320 | ); | 370 | ); |
| 321 | runOnlyForDeploymentPostprocessing = 0; | 371 | runOnlyForDeploymentPostprocessing = 0; |
| 322 | }; | 372 | }; |
| 373 | 8BDC00010000000000000009 /* Resources */ = { | ||
| 374 | isa = PBXResourcesBuildPhase; | ||
| 375 | buildActionMask = 2147483647; | ||
| 376 | files = ( | ||
| 377 | ); | ||
| 378 | runOnlyForDeploymentPostprocessing = 0; | ||
| 379 | }; | ||
| 323 | /* End PBXResourcesBuildPhase section */ | 380 | /* End PBXResourcesBuildPhase section */ |
| 324 | 381 | ||
| 325 | /* Begin PBXSourcesBuildPhase section */ | 382 | /* Begin PBXSourcesBuildPhase section */ |
| @@ -351,6 +408,13 @@ | |||
| 351 | ); | 408 | ); |
| 352 | runOnlyForDeploymentPostprocessing = 0; | 409 | runOnlyForDeploymentPostprocessing = 0; |
| 353 | }; | 410 | }; |
| 411 | 8BDC00010000000000000007 /* Sources */ = { | ||
| 412 | isa = PBXSourcesBuildPhase; | ||
| 413 | buildActionMask = 2147483647; | ||
| 414 | files = ( | ||
| 415 | ); | ||
| 416 | runOnlyForDeploymentPostprocessing = 0; | ||
| 417 | }; | ||
| 354 | /* End PBXSourcesBuildPhase section */ | 418 | /* End PBXSourcesBuildPhase section */ |
| 355 | 419 | ||
| 356 | /* Begin PBXTargetDependency section */ | 420 | /* Begin PBXTargetDependency section */ |
| @@ -364,6 +428,11 @@ | |||
| 364 | target = 8BDB00010000000000000006 /* DomainDigShareExtension */; | 428 | target = 8BDB00010000000000000006 /* DomainDigShareExtension */; |
| 365 | targetProxy = 8BDB00010000000000000003 /* PBXContainerItemProxy */; | 429 | targetProxy = 8BDB00010000000000000003 /* PBXContainerItemProxy */; |
| 366 | }; | 430 | }; |
| 431 | 8BDC00010000000000000004 /* PBXTargetDependency */ = { | ||
| 432 | isa = PBXTargetDependency; | ||
| 433 | target = 8B7800682F6090E300933221 /* DomainDig */; | ||
| 434 | targetProxy = 8BDC00010000000000000003 /* PBXContainerItemProxy */; | ||
| 435 | }; | ||
| 367 | /* End PBXTargetDependency section */ | 436 | /* End PBXTargetDependency section */ |
| 368 | 437 | ||
| 369 | /* Begin XCBuildConfiguration section */ | 438 | /* Begin XCBuildConfiguration section */ |
| @@ -420,7 +489,7 @@ | |||
| 420 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; | 489 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; |
| 421 | GCC_WARN_UNUSED_FUNCTION = YES; | 490 | GCC_WARN_UNUSED_FUNCTION = YES; |
| 422 | GCC_WARN_UNUSED_VARIABLE = YES; | 491 | GCC_WARN_UNUSED_VARIABLE = YES; |
| 423 | IPHONEOS_DEPLOYMENT_TARGET = 26.2; | 492 | IPHONEOS_DEPLOYMENT_TARGET = 17.6; |
| 424 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; | 493 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; |
| 425 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; | 494 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; |
| 426 | MTL_FAST_MATH = YES; | 495 | MTL_FAST_MATH = YES; |
| @@ -479,7 +548,7 @@ | |||
| 479 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; | 548 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; |
| 480 | GCC_WARN_UNUSED_FUNCTION = YES; | 549 | GCC_WARN_UNUSED_FUNCTION = YES; |
| 481 | GCC_WARN_UNUSED_VARIABLE = YES; | 550 | GCC_WARN_UNUSED_VARIABLE = YES; |
| 482 | IPHONEOS_DEPLOYMENT_TARGET = 26.2; | 551 | IPHONEOS_DEPLOYMENT_TARGET = 17.6; |
| 483 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; | 552 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; |
| 484 | MTL_ENABLE_DEBUG_INFO = NO; | 553 | MTL_ENABLE_DEBUG_INFO = NO; |
| 485 | MTL_FAST_MATH = YES; | 554 | MTL_FAST_MATH = YES; |
| @@ -680,6 +749,44 @@ | |||
| 680 | }; | 749 | }; |
| 681 | name = Release; | 750 | name = Release; |
| 682 | }; | 751 | }; |
| 752 | 8BDC0001000000000000000B /* Debug */ = { | ||
| 753 | isa = XCBuildConfiguration; | ||
| 754 | buildSettings = { | ||
| 755 | CODE_SIGN_STYLE = Automatic; | ||
| 756 | CURRENT_PROJECT_VERSION = 43; | ||
| 757 | DEVELOPMENT_TEAM = ZCNAX3VL9D; | ||
| 758 | GENERATE_INFOPLIST_FILE = YES; | ||
| 759 | IPHONEOS_DEPLOYMENT_TARGET = 17.6; | ||
| 760 | MARKETING_VERSION = 4.8.3; | ||
| 761 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDigUITests; | ||
| 762 | PRODUCT_NAME = "$(TARGET_NAME)"; | ||
| 763 | SWIFT_APPROACHABLE_CONCURRENCY = YES; | ||
| 764 | SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; | ||
| 765 | SWIFT_VERSION = 5.0; | ||
| 766 | TARGETED_DEVICE_FAMILY = "1,2"; | ||
| 767 | TEST_TARGET_NAME = DomainDig; | ||
| 768 | }; | ||
| 769 | name = Debug; | ||
| 770 | }; | ||
| 771 | 8BDC0001000000000000000C /* Release */ = { | ||
| 772 | isa = XCBuildConfiguration; | ||
| 773 | buildSettings = { | ||
| 774 | CODE_SIGN_STYLE = Automatic; | ||
| 775 | CURRENT_PROJECT_VERSION = 43; | ||
| 776 | DEVELOPMENT_TEAM = ZCNAX3VL9D; | ||
| 777 | GENERATE_INFOPLIST_FILE = YES; | ||
| 778 | IPHONEOS_DEPLOYMENT_TARGET = 17.6; | ||
| 779 | MARKETING_VERSION = 4.8.3; | ||
| 780 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDigUITests; | ||
| 781 | PRODUCT_NAME = "$(TARGET_NAME)"; | ||
| 782 | SWIFT_APPROACHABLE_CONCURRENCY = YES; | ||
| 783 | SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; | ||
| 784 | SWIFT_VERSION = 5.0; | ||
| 785 | TARGETED_DEVICE_FAMILY = "1,2"; | ||
| 786 | TEST_TARGET_NAME = DomainDig; | ||
| 787 | }; | ||
| 788 | name = Release; | ||
| 789 | }; | ||
| 683 | /* End XCBuildConfiguration section */ | 790 | /* End XCBuildConfiguration section */ |
| 684 | 791 | ||
| 685 | /* Begin XCConfigurationList section */ | 792 | /* Begin XCConfigurationList section */ |
| @@ -719,6 +826,15 @@ | |||
| 719 | defaultConfigurationIsVisible = 0; | 826 | defaultConfigurationIsVisible = 0; |
| 720 | defaultConfigurationName = Release; | 827 | defaultConfigurationName = Release; |
| 721 | }; | 828 | }; |
| 829 | 8BDC0001000000000000000A /* Build configuration list for PBXNativeTarget "DomainDigUITests" */ = { | ||
| 830 | isa = XCConfigurationList; | ||
| 831 | buildConfigurations = ( | ||
| 832 | 8BDC0001000000000000000B /* Debug */, | ||
| 833 | 8BDC0001000000000000000C /* Release */, | ||
| 834 | ); | ||
| 835 | defaultConfigurationIsVisible = 0; | ||
| 836 | defaultConfigurationName = Release; | ||
| 837 | }; | ||
| 722 | /* End XCConfigurationList section */ | 838 | /* End XCConfigurationList section */ |
| 723 | }; | 839 | }; |
| 724 | rootObject = 8B7800612F6090E300933221 /* Project object */; | 840 | rootObject = 8B7800612F6090E300933221 /* Project object */; |
DomainDig.xcodeproj/xcshareddata/xcschemes/DomainDig.xcscheme +12
| @@ -29,6 +29,18 @@ | |||
| 29 | selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" | 29 | selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" |
| 30 | shouldUseLaunchSchemeArgsEnv = "YES" | 30 | shouldUseLaunchSchemeArgsEnv = "YES" |
| 31 | shouldAutocreateTestPlan = "YES"> | 31 | shouldAutocreateTestPlan = "YES"> |
| 32 | <Testables> | ||
| 33 | <TestableReference | ||
| 34 | skipped = "NO"> | ||
| 35 | <BuildableReference | ||
| 36 | BuildableIdentifier = "primary" | ||
| 37 | BlueprintIdentifier = "8BDC00010000000000000006" | ||
| 38 | BuildableName = "DomainDigUITests.xctest" | ||
| 39 | BlueprintName = "DomainDigUITests" | ||
| 40 | ReferencedContainer = "container:DomainDig.xcodeproj"> | ||
| 41 | </BuildableReference> | ||
| 42 | </TestableReference> | ||
| 43 | </Testables> | ||
| 32 | </TestAction> | 44 | </TestAction> |
| 33 | <LaunchAction | 45 | <LaunchAction |
| 34 | buildConfiguration = "Debug" | 46 | buildConfiguration = "Debug" |
DomainDigUITests/AccessibilityAuditHarness.swift added +121
| @@ -0,0 +1,121 @@ | |||
| 1 | import XCTest | ||
| 2 | |||
| 3 | /// Shared plumbing for the accessibility audit suite. | ||
| 4 | /// | ||
| 5 | /// `performAccessibilityAudit` checks contrast, hit-region size, clipped text at | ||
| 6 | /// large Dynamic Type, element descriptions, and trait correctness — the same | ||
| 7 | /// categories the accessibility pass in issue #21 works through. | ||
| 8 | /// | ||
| 9 | /// **The suite reports by default and fails only for enforced categories.** The | ||
| 10 | /// audit surfaces violations that exist today, so failing on everything would | ||
| 11 | /// block unrelated PRs until the whole pass lands. `enforcedAuditTypes` below is | ||
| 12 | /// the ratchet: widen it as each phase of #21 clears a category. | ||
| 13 | /// | ||
| 14 | /// Two alternatives were tried and rejected: | ||
| 15 | /// | ||
| 16 | /// - *A per-screen baseline count.* Audit coverage is not nested across OS | ||
| 17 | /// versions — the same screen legitimately yields different counts on the | ||
| 18 | /// floor simulator and the current one, so no single committed number is | ||
| 19 | /// correct for both. | ||
| 20 | /// - *An environment variable.* Neither a plain `xcodebuild` env var nor a | ||
| 21 | /// `TEST_RUNNER_`-prefixed build setting reaches this process, so the toggle | ||
| 22 | /// silently did nothing. A committed constant also makes "when did contrast | ||
| 23 | /// become enforced?" answerable with `git blame` instead of CI tribal | ||
| 24 | /// knowledge. | ||
| 25 | @MainActor | ||
| 26 | enum AccessibilityAuditHarness { | ||
| 27 | /// Launch argument that lifts feature gating so Pro-only screens are | ||
| 28 | /// reachable. `PurchaseService` honours this in `DEBUG` builds only. | ||
| 29 | private static let forceProPlusArgument = "DOMAIN_DIG_FORCE_PRO_PLUS" | ||
| 30 | |||
| 31 | /// Audit categories that fail the build. Everything else is reported only. | ||
| 32 | /// | ||
| 33 | /// Empty until the accessibility pass starts landing. Suggested ratchet, | ||
| 34 | /// following the phases in issue #21: | ||
| 35 | /// | ||
| 36 | /// - after phase 2 (semantic colors + light mode): `.contrast` | ||
| 37 | /// - after phase 3 (Dynamic Type + reflow): `.textClipped`, `.dynamicType`, | ||
| 38 | /// `.hitRegion` | ||
| 39 | /// - after phase 4 (VoiceOver): `.elementDetection`, | ||
| 40 | /// `.sufficientElementDescription`, `.trait` | ||
| 41 | static let enforcedAuditTypes: XCUIAccessibilityAuditType = [] | ||
| 42 | |||
| 43 | /// Launches the app with feature gating lifted, optionally at a specific | ||
| 44 | /// content size category. | ||
| 45 | static func launch(contentSizeCategory: String? = nil) -> XCUIApplication { | ||
| 46 | let app = XCUIApplication() | ||
| 47 | app.launchArguments = [forceProPlusArgument] | ||
| 48 | if let contentSizeCategory { | ||
| 49 | app.launchArguments += ["-UIPreferredContentSizeCategoryName", contentSizeCategory] | ||
| 50 | } | ||
| 51 | app.launch() | ||
| 52 | return app | ||
| 53 | } | ||
| 54 | |||
| 55 | /// Runs a full audit and records every finding against the test. | ||
| 56 | /// | ||
| 57 | /// Findings are logged and attached to the result bundle so a CI run | ||
| 58 | /// produces the burndown list as an artifact rather than only a pass/fail. | ||
| 59 | static func audit( | ||
| 60 | _ app: XCUIApplication, | ||
| 61 | screen: String, | ||
| 62 | test: XCTestCase | ||
| 63 | ) throws { | ||
| 64 | var findings: [String] = [] | ||
| 65 | |||
| 66 | try app.performAccessibilityAudit { issue in | ||
| 67 | let isEnforced = !enforcedAuditTypes.intersection(issue.auditType).isEmpty | ||
| 68 | let marker = isEnforced ? "FAIL" : "report" | ||
| 69 | findings.append("[\(marker)][\(name(for: issue.auditType))] \(issue.compactDescription)") | ||
| 70 | // true suppresses the finding, false reports it as a test failure. | ||
| 71 | return !isEnforced | ||
| 72 | } | ||
| 73 | |||
| 74 | let summary = findings.isEmpty | ||
| 75 | ? "\(screen): no accessibility findings" | ||
| 76 | : "\(screen): \(findings.count) finding(s)\n" + findings.sorted().map { " • \($0)" }.joined(separator: "\n") | ||
| 77 | |||
| 78 | print(summary) | ||
| 79 | |||
| 80 | let attachment = XCTAttachment(string: summary) | ||
| 81 | attachment.name = "a11y-audit-\(screen)" | ||
| 82 | attachment.lifetime = .keepAlways | ||
| 83 | test.add(attachment) | ||
| 84 | } | ||
| 85 | |||
| 86 | /// `XCUIAccessibilityAuditType` is an option set whose description is just a | ||
| 87 | /// raw bitmask, which makes the burndown list unreadable. Resolve it against | ||
| 88 | /// the named members rather than hard-coding bit positions, so this keeps | ||
| 89 | /// working if Apple adds audit types. | ||
| 90 | private static func name(for type: XCUIAccessibilityAuditType) -> String { | ||
| 91 | let known: [(XCUIAccessibilityAuditType, String)] = [ | ||
| 92 | (.contrast, "contrast"), | ||
| 93 | (.elementDetection, "elementDetection"), | ||
| 94 | (.hitRegion, "hitRegion"), | ||
| 95 | (.sufficientElementDescription, "sufficientElementDescription"), | ||
| 96 | (.dynamicType, "dynamicType"), | ||
| 97 | (.textClipped, "textClipped"), | ||
| 98 | (.trait, "trait") | ||
| 99 | ] | ||
| 100 | let matched = known.filter { type.contains($0.0) }.map(\.1) | ||
| 101 | return matched.isEmpty ? "unknown(\(type.rawValue))" : matched.joined(separator: "+") | ||
| 102 | } | ||
| 103 | } | ||
| 104 | |||
| 105 | extension XCUIApplication { | ||
| 106 | /// Taps a root tab by its visible label. | ||
| 107 | /// | ||
| 108 | /// Falls back to a plain button query because the tab bar is only present in | ||
| 109 | /// the compact size class — in regular width `RootTabView` renders a | ||
| 110 | /// `NavigationSplitView` sidebar instead. | ||
| 111 | @MainActor | ||
| 112 | func selectRootTab(_ name: String) { | ||
| 113 | let tabButton = tabBars.buttons[name] | ||
| 114 | let element = tabButton.waitForExistence(timeout: 5) ? tabButton : buttons[name] | ||
| 115 | XCTAssertTrue( | ||
| 116 | element.waitForExistence(timeout: 5), | ||
| 117 | "Could not find a way to reach the \(name) screen" | ||
| 118 | ) | ||
| 119 | element.tap() | ||
| 120 | } | ||
| 121 | } | ||
DomainDigUITests/AccessibilityAuditTests.swift added +84
| @@ -0,0 +1,84 @@ | |||
| 1 | import XCTest | ||
| 2 | |||
| 3 | /// One accessibility audit per primary screen, plus a Dynamic Type sweep. | ||
| 4 | /// | ||
| 5 | /// See `AccessibilityAuditHarness` for why these report rather than fail by | ||
| 6 | /// default, and how to make them enforcing. | ||
| 7 | @MainActor | ||
| 8 | final class AccessibilityAuditTests: XCTestCase { | ||
| 9 | override func setUp() { | ||
| 10 | // Keep going after a failure so an enforced audit still collects and | ||
| 11 | // attaches every finding. With this off, XCTest aborts at the first | ||
| 12 | // reported issue and the burndown list is lost precisely when a category | ||
| 13 | // is being enforced. | ||
| 14 | continueAfterFailure = true | ||
| 15 | } | ||
| 16 | |||
| 17 | // MARK: Per-screen audits | ||
| 18 | |||
| 19 | func testInspectScreen() throws { | ||
| 20 | let app = AccessibilityAuditHarness.launch() | ||
| 21 | app.selectRootTab("Inspect") | ||
| 22 | try AccessibilityAuditHarness.audit(app, screen: "inspect", test: self) | ||
| 23 | } | ||
| 24 | |||
| 25 | func testDashboardScreen() throws { | ||
| 26 | let app = AccessibilityAuditHarness.launch() | ||
| 27 | app.selectRootTab("Dashboard") | ||
| 28 | try AccessibilityAuditHarness.audit(app, screen: "dashboard", test: self) | ||
| 29 | } | ||
| 30 | |||
| 31 | func testAuditScreen() throws { | ||
| 32 | let app = AccessibilityAuditHarness.launch() | ||
| 33 | app.selectRootTab("Audit") | ||
| 34 | try AccessibilityAuditHarness.audit(app, screen: "audit", test: self) | ||
| 35 | } | ||
| 36 | |||
| 37 | func testHistoryScreen() throws { | ||
| 38 | let app = AccessibilityAuditHarness.launch() | ||
| 39 | app.selectRootTab("History") | ||
| 40 | try AccessibilityAuditHarness.audit(app, screen: "history", test: self) | ||
| 41 | } | ||
| 42 | |||
| 43 | func testSettingsScreen() throws { | ||
| 44 | let app = AccessibilityAuditHarness.launch() | ||
| 45 | app.selectRootTab("Settings") | ||
| 46 | try AccessibilityAuditHarness.audit(app, screen: "settings", test: self) | ||
| 47 | } | ||
| 48 | |||
| 49 | func testTrackedDomainsScreen() throws { | ||
| 50 | let app = AccessibilityAuditHarness.launch() | ||
| 51 | app.selectRootTab("Settings") | ||
| 52 | |||
| 53 | let trackedDomains = app.buttons["Tracked Domains"] | ||
| 54 | XCTAssertTrue( | ||
| 55 | trackedDomains.waitForExistence(timeout: 5), | ||
| 56 | "Settings no longer offers a Tracked Domains row" | ||
| 57 | ) | ||
| 58 | trackedDomains.tap() | ||
| 59 | |||
| 60 | try AccessibilityAuditHarness.audit(app, screen: "tracked-domains", test: self) | ||
| 61 | } | ||
| 62 | |||
| 63 | // MARK: Dynamic Type | ||
| 64 | |||
| 65 | /// Re-audits every root screen at the largest accessibility content size. | ||
| 66 | /// | ||
| 67 | /// This is where clipped text and fixed-height containers surface — the | ||
| 68 | /// `.accessibility5`-class failures that the fixed geometry in | ||
| 69 | /// `AppDensityMetrics` is expected to produce until phase 3 of #21 lands. | ||
| 70 | func testAllScreensAtLargestAccessibilitySize() throws { | ||
| 71 | let app = AccessibilityAuditHarness.launch( | ||
| 72 | contentSizeCategory: "UICTContentSizeCategoryAccessibilityXXXL" | ||
| 73 | ) | ||
| 74 | |||
| 75 | for tab in ["Inspect", "Dashboard", "Audit", "History", "Settings"] { | ||
| 76 | app.selectRootTab(tab) | ||
| 77 | try AccessibilityAuditHarness.audit( | ||
| 78 | app, | ||
| 79 | screen: "\(tab.lowercased())-accessibilityXXXL", | ||
| 80 | test: self | ||
| 81 | ) | ||
| 82 | } | ||
| 83 | } | ||
| 84 | } | ||