summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-07-22 00:47:12 -0500
committerChristian Cleberg <[email protected]>2026-07-22 15:04:32 -0500
commit7f917e98b929dcf0f7901d0bc4eb05e04db3aa0c (patch)
tree6956220f6513e0eb6705b1707b6df9cdeb982ca6
parent80cb5d8553d3afd097b10e83cf7af0bf1ba56523 (diff)
downloaddomain-dig-7f917e98b929dcf0f7901d0bc4eb05e04db3aa0c.tar.gz
domain-dig-7f917e98b929dcf0f7901d0bc4eb05e04db3aa0c.tar.bz2
domain-dig-7f917e98b929dcf0f7901d0bc4eb05e04db3aa0c.zip
feat(a11y): seeded audit fixtures; fix dense-row reflow they exposed (#21)
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.
-rw-r--r--DomainDig/AuditFixtures.swift168
-rw-r--r--DomainDig/BatchResultsView.swift41
-rw-r--r--DomainDig/DashboardView.swift32
-rw-r--r--DomainDig/DomainDigApp.swift3
-rw-r--r--DomainDig/DomainDigUI.swift5
-rw-r--r--DomainDig/DomainViewModel+Widget.swift5
-rw-r--r--DomainDig/DomainViewModel.swift27
-rw-r--r--DomainDig/WatchlistView.swift65
-rw-r--r--DomainDigUITests/AccessibilityAuditHarness.swift13
-rw-r--r--DomainDigUITests/AccessibilityAuditTests.swift65
10 files changed, 381 insertions, 43 deletions
diff --git a/DomainDig/AuditFixtures.swift b/DomainDig/AuditFixtures.swift
new file mode 100644
index 0000000..0a9655b
--- /dev/null
+++ b/DomainDig/AuditFixtures.swift
@@ -0,0 +1,168 @@
+#if DEBUG
+import Foundation
+
+/// Deterministic in-memory fixtures for the accessibility audit suite.
+///
+/// The dense rows (`WatchlistRowView`, `BatchResultRowView`) and the Dashboard
+/// portfolio sections never render on a fresh simulator, so five phases of row
+/// treatment shipped unverified by the automated audit. Driving the add-domain
+/// UI instead was tried and rejected: typing raises the keyboard, which then
+/// follows the audit onto later screens, and the added domains persist across
+/// runs, contaminating every other test's baseline.
+///
+/// These are activated by the `DOMAIN_DIG_SEED_FIXTURES` launch argument
+/// (DEBUG builds only, same pattern as `DOMAIN_DIG_FORCE_PRO_PLUS`) and are
+/// **never persisted** — see `seedAuditFixturesIfRequested()`.
+///
+/// The set is chosen to exercise every row path: healthy/warning/critical
+/// certificate badges, pinned, noted, changed, monitoring on/off, a
+/// stress-length domain name, and every batch status including failure.
+enum AuditFixtures {
+ static let launchArgument = "DOMAIN_DIG_SEED_FIXTURES"
+
+ static var requested: Bool {
+ ProcessInfo.processInfo.arguments.contains(launchArgument)
+ }
+
+ /// Relative to launch so the recency-gated Dashboard sections (Recent
+ /// Activity, Attention Required — both keyed to the last 24h) actually
+ /// render. Label text varies run to run ("2 hr. ago"); the audit measures
+ /// layout and traits, not string equality, so coverage wins.
+ private static let now = Date()
+
+ static var trackedDomains: [TrackedDomain] {
+ [
+ TrackedDomain(
+ domain: "healthy.example",
+ createdAt: now.addingTimeInterval(-86_400 * 30),
+ updatedAt: now.addingTimeInterval(-3_600),
+ isPinned: true,
+ monitoringEnabled: true,
+ lastKnownAvailability: .registered,
+ certificateWarningLevel: .none,
+ certificateDaysRemaining: 240,
+ lastMonitoredAt: now.addingTimeInterval(-1_800)
+ ),
+ TrackedDomain(
+ domain: "expiring.example",
+ createdAt: now.addingTimeInterval(-86_400 * 90),
+ updatedAt: now.addingTimeInterval(-7_200),
+ monitoringEnabled: true,
+ lastKnownAvailability: .registered,
+ lastChangeSummary: DomainChangeSummary(
+ hasChanges: true,
+ changedSections: ["ssl"],
+ message: "Certificate is approaching expiry",
+ severity: .medium,
+ impactClassification: .warning,
+ generatedAt: now.addingTimeInterval(-7_200)
+ ),
+ lastChangeSeverity: .medium,
+ certificateWarningLevel: .warning,
+ certificateDaysRemaining: 12,
+ lastMonitoredAt: now.addingTimeInterval(-7_200),
+ lastAlertAt: now.addingTimeInterval(-7_000)
+ ),
+ TrackedDomain(
+ domain: "broken.example",
+ createdAt: now.addingTimeInterval(-86_400 * 7),
+ updatedAt: now.addingTimeInterval(-600),
+ note: "Production incident follow-up: certificate replaced?",
+ monitoringEnabled: false,
+ lastKnownAvailability: .registered,
+ lastChangeSummary: DomainChangeSummary(
+ hasChanges: true,
+ changedSections: ["ssl", "dns"],
+ message: "TLS validation failed and NS records changed",
+ severity: .high,
+ impactClassification: .critical,
+ generatedAt: now.addingTimeInterval(-600)
+ ),
+ lastChangeSeverity: .high,
+ certificateWarningLevel: .critical,
+ certificateDaysRemaining: -3
+ ),
+ TrackedDomain(
+ domain: "very-long-subdomain.observability.internal.staging.example",
+ createdAt: now.addingTimeInterval(-86_400),
+ updatedAt: now.addingTimeInterval(-60),
+ monitoringEnabled: true,
+ lastKnownAvailability: .unknown
+ )
+ ]
+ }
+
+ static var batchResults: [BatchLookupResult] {
+ [
+ BatchLookupResult(
+ domain: "healthy.example",
+ historyEntryID: nil,
+ resultSource: .live,
+ availability: .registered,
+ primaryIP: "203.0.113.10",
+ quickStatus: "Stable",
+ summaryMessage: nil,
+ changeSeverity: nil,
+ changeClassification: nil,
+ certificateWarningLevel: .none,
+ riskScore: 12,
+ riskLevel: .low,
+ timestamp: now.addingTimeInterval(-120),
+ status: .completed,
+ errorMessage: nil
+ ),
+ BatchLookupResult(
+ domain: "expiring.example",
+ historyEntryID: nil,
+ resultSource: .cached,
+ availability: .registered,
+ primaryIP: "203.0.113.11",
+ quickStatus: "Changed",
+ summaryMessage: "Certificate is approaching expiry",
+ changeSeverity: .medium,
+ changeClassification: .warning,
+ certificateWarningLevel: .warning,
+ riskScore: 41,
+ riskLevel: .medium,
+ timestamp: now.addingTimeInterval(-3_600),
+ status: .completed,
+ errorMessage: nil
+ ),
+ BatchLookupResult(
+ domain: "broken.example",
+ historyEntryID: nil,
+ resultSource: .live,
+ availability: .registered,
+ primaryIP: "2001:db8::1f3:44",
+ quickStatus: "Changed",
+ summaryMessage: "TLS validation failed",
+ changeSeverity: .high,
+ changeClassification: .critical,
+ certificateWarningLevel: .critical,
+ riskScore: 78,
+ riskLevel: .high,
+ timestamp: now.addingTimeInterval(-60),
+ status: .completed,
+ errorMessage: nil
+ ),
+ BatchLookupResult(
+ domain: "unreachable.example",
+ historyEntryID: nil,
+ resultSource: .live,
+ availability: nil,
+ primaryIP: nil,
+ quickStatus: "Failed",
+ summaryMessage: nil,
+ changeSeverity: nil,
+ changeClassification: nil,
+ certificateWarningLevel: .none,
+ riskScore: nil,
+ riskLevel: nil,
+ timestamp: now,
+ status: .failed,
+ errorMessage: "The lookup timed out before any records were returned"
+ )
+ ]
+ }
+}
+#endif
diff --git a/DomainDig/BatchResultsView.swift b/DomainDig/BatchResultsView.swift
index aeae48f..1636a16 100644
--- a/DomainDig/BatchResultsView.swift
+++ b/DomainDig/BatchResultsView.swift
@@ -59,16 +59,22 @@ struct BatchResultRowView: View {
var body: some View {
VStack(alignment: .leading, spacing: appDensity.metrics.rowSpacing + 1) {
- HStack(alignment: .firstTextBaseline, spacing: 8) {
- Text(result.domain)
- .font(appDensity.font(.callout))
- .foregroundStyle(.primary)
- .lineLimit(1)
- Spacer(minLength: 8)
- Text(result.resultSource.label.lowercased())
- .font(appDensity.font(.caption2))
- .foregroundStyle(Color(.appTextSecondary))
- AppStatusBadgeView(model: quickStatusBadge)
+ // Same reflow as WatchlistRowView: wide while it fits, stacked at
+ // accessibility sizes so the badge cannot letter-wrap vertically.
+ ViewThatFits(in: .horizontal) {
+ HStack(alignment: .firstTextBaseline, spacing: 8) {
+ domainTitle
+ Spacer(minLength: 8)
+ sourceLabel
+ AppStatusBadgeView(model: quickStatusBadge)
+ }
+ VStack(alignment: .leading, spacing: 6) {
+ domainTitle
+ HStack(spacing: 8) {
+ AppStatusBadgeView(model: quickStatusBadge)
+ sourceLabel
+ }
+ }
}
HStack(spacing: 10) {
@@ -133,6 +139,21 @@ struct BatchResultRowView: View {
))
}
+ private var domainTitle: some View {
+ Text(result.domain)
+ .font(appDensity.font(.callout))
+ .foregroundStyle(.primary)
+ .lineLimit(3)
+ .multilineTextAlignment(.leading)
+ .fixedSize(horizontal: false, vertical: true)
+ }
+
+ private var sourceLabel: some View {
+ Text(result.resultSource.label.lowercased())
+ .font(appDensity.font(.caption2))
+ .foregroundStyle(Color(.appTextSecondary))
+ }
+
private var changeContentLabel: String {
result.changeClassification != nil ? "Impact" : "Status"
}
diff --git a/DomainDig/DashboardView.swift b/DomainDig/DashboardView.swift
index d86e591..3111d75 100644
--- a/DomainDig/DashboardView.swift
+++ b/DomainDig/DashboardView.swift
@@ -390,21 +390,33 @@ private struct PortfolioExpiryRow: View {
let state: PortfolioDomainStatus
var body: some View {
- HStack {
- VStack(alignment: .leading, spacing: 4) {
- Text(state.trackedDomain.domain)
- .font(appDensity.font(.callout))
- .foregroundStyle(.primary)
- Text(expirySubtitle)
- .font(appDensity.font(.caption))
- .foregroundStyle(Color(.appTextSecondary))
+ // Wide while it fits; stacked at accessibility sizes so the badge does
+ // not letter-wrap beside a long domain.
+ ViewThatFits(in: .horizontal) {
+ HStack {
+ expiryText
+ Spacer()
+ AppStatusBadgeView(model: badgeModel)
+ }
+ VStack(alignment: .leading, spacing: 6) {
+ expiryText
+ AppStatusBadgeView(model: badgeModel)
}
- Spacer()
- AppStatusBadgeView(model: badgeModel)
}
.padding(.vertical, 4)
}
+ private var expiryText: some View {
+ VStack(alignment: .leading, spacing: 4) {
+ Text(state.trackedDomain.domain)
+ .font(appDensity.font(.callout))
+ .foregroundStyle(.primary)
+ Text(expirySubtitle)
+ .font(appDensity.font(.caption))
+ .foregroundStyle(Color(.appTextSecondary))
+ }
+ }
+
private var expirySubtitle: String {
if let days = state.certificateDaysRemaining {
return "Expires in \(days) day\(days == 1 ? "" : "s")"
diff --git a/DomainDig/DomainDigApp.swift b/DomainDig/DomainDigApp.swift
index d392dde..914dbc8 100644
--- a/DomainDig/DomainDigApp.swift
+++ b/DomainDig/DomainDigApp.swift
@@ -31,6 +31,9 @@ struct DomainDigApp: App {
// The single place appearance is applied. Keep it that way.
.preferredColorScheme((AppAppearance(rawValue: appearance) ?? .system).colorScheme)
.task {
+ #if DEBUG
+ viewModel.seedAuditFixturesIfRequested()
+ #endif
let _ = purchaseService.currentTier
let _ = cloudSyncService.status
let _ = localAPIService.isRunning
diff --git a/DomainDig/DomainDigUI.swift b/DomainDig/DomainDigUI.swift
index a50f766..67c30b4 100644
--- a/DomainDig/DomainDigUI.swift
+++ b/DomainDig/DomainDigUI.swift
@@ -245,6 +245,11 @@ struct AppStatusBadgeView: View {
}
Text(model.title)
}
+ // Never compress. Squeezed beside a long domain at accessibility sizes,
+ // the capsule otherwise wraps one character per line into a
+ // screen-height pill. Taking natural width instead forces the row's
+ // ViewThatFits onto its stacked layout, which is the intended fallback.
+ .fixedSize()
.font(appDensity.font(.caption, weight: .semibold))
.foregroundStyle(model.foregroundColor)
.padding(.horizontal, 9)
diff --git a/DomainDig/DomainViewModel+Widget.swift b/DomainDig/DomainViewModel+Widget.swift
index 59b28e0..69640d8 100644
--- a/DomainDig/DomainViewModel+Widget.swift
+++ b/DomainDig/DomainViewModel+Widget.swift
@@ -5,6 +5,11 @@ extension DomainViewModel {
/// Publishes the current portfolio state to the App Group container so the
/// widget can render it, then asks WidgetKit to refresh its timelines.
func refreshWidgetData() {
+ #if DEBUG
+ // Fixture sessions must not write fixture domains into the shared
+ // widget store — it is an App Group file that outlives the launch.
+ if auditFixturesActive { return }
+ #endif
let data = portfolioDashboardData
let snapshot = data.snapshot
diff --git a/DomainDig/DomainViewModel.swift b/DomainDig/DomainViewModel.swift
index de17cbc..b5f51c3 100644
--- a/DomainDig/DomainViewModel.swift
+++ b/DomainDig/DomainViewModel.swift
@@ -1019,6 +1019,11 @@ final class DomainViewModel {
}
func refreshMonitoringState() {
+ #if DEBUG
+ // Runs right after fixture seeding in the app task (and again on every
+ // scene activation); the disk reload below would wipe the fixtures.
+ if auditFixturesActive { return }
+ #endif
DataMigrationService.migrateIfNeeded()
trackedDomains = Self.loadTrackedDomains()
history = Self.loadHistoryEntries()
@@ -1036,6 +1041,10 @@ final class DomainViewModel {
}
func refreshPersistedData() {
+ #if DEBUG
+ // A reload from disk would silently replace the in-memory fixtures.
+ if auditFixturesActive { return }
+ #endif
recentSearches = DomainDataPortabilityService.loadRecentSearches()
savedDomains = DomainDataPortabilityService.loadSavedDomains()
trackedDomains = Self.loadTrackedDomains()
@@ -2614,7 +2623,25 @@ final class DomainViewModel {
persistHistory()
}
+ #if DEBUG
+ /// True when this session was launched with `DOMAIN_DIG_SEED_FIXTURES`.
+ /// Blocks tracked-domain persistence, widget-store writes, and persisted-data
+ /// reloads so fixture data stays strictly in-memory — the audit suite relies
+ /// on every launch starting from the same state.
+ private(set) var auditFixturesActive = false
+
+ func seedAuditFixturesIfRequested() {
+ guard AuditFixtures.requested, !auditFixturesActive else { return }
+ auditFixturesActive = true
+ trackedDomains = AuditFixtures.trackedDomains
+ batchResults = AuditFixtures.batchResults
+ }
+ #endif
+
private func persistTrackedDomains() {
+ #if DEBUG
+ if auditFixturesActive { return }
+ #endif
if trackedDomainsPersistenceSuspended {
trackedDomainsPersistenceDirty = true
return
diff --git a/DomainDig/WatchlistView.swift b/DomainDig/WatchlistView.swift
index db79b0b..eb934e6 100644
--- a/DomainDig/WatchlistView.swift
+++ b/DomainDig/WatchlistView.swift
@@ -435,19 +435,20 @@ struct WatchlistRowView: View {
var body: some View {
VStack(alignment: .leading, spacing: appDensity.metrics.rowSpacing + 1) {
- HStack(alignment: .firstTextBaseline, spacing: 8) {
- if trackedDomain.isPinned {
- Image(systemName: "pin.fill")
- .font(.caption2)
- .foregroundStyle(Color(.statusWarning))
- }
- Text(trackedDomain.domain)
- .font(appDensity.font(.callout))
- .foregroundStyle(.primary)
- .lineLimit(2)
- .multilineTextAlignment(.leading)
- Spacer(minLength: 8)
- statusBadge
+ // Side-by-side while it fits; at accessibility sizes the badge drops
+ // below the domain instead of squeezing it into "hea lt…" while the
+ // badge letter-wraps down the screen. ViewThatFits picks the wide
+ // layout whenever it genuinely fits, so default sizes keep density.
+ ViewThatFits(in: .horizontal) {
+ HStack(alignment: .firstTextBaseline, spacing: 8) {
+ domainTitle
+ Spacer(minLength: 8)
+ statusBadge
+ }
+ VStack(alignment: .leading, spacing: 6) {
+ domainTitle
+ statusBadge
+ }
}
Text("Updated \(trackedDomain.updatedAt.formatted(date: .abbreviated, time: .shortened))")
@@ -460,14 +461,9 @@ struct WatchlistRowView: View {
.foregroundStyle(Color(.appTextSecondary))
}
- HStack(spacing: 8) {
- Text(trackedDomain.monitoringEnabled ? "Monitoring on" : "Monitoring off")
- if let lastMonitoredAt = trackedDomain.lastMonitoredAt {
- Text("Checked \(lastMonitoredAt.formatted(date: .omitted, time: .shortened))")
- }
- if let lastAlertAt = trackedDomain.lastAlertAt {
- Text("Alert \(lastAlertAt.formatted(date: .omitted, time: .shortened))")
- }
+ ViewThatFits(in: .horizontal) {
+ HStack(spacing: 8) { monitoringMetadata }
+ VStack(alignment: .leading, spacing: 2) { monitoringMetadata }
}
.font(appDensity.font(.caption2))
.foregroundStyle(Color(.appTextSecondary))
@@ -491,6 +487,33 @@ struct WatchlistRowView: View {
.modifier(WatchlistRowAccessibility(trackedDomain: trackedDomain, isRefreshing: isRefreshing))
}
+ @ViewBuilder
+ private var monitoringMetadata: some View {
+ Text(trackedDomain.monitoringEnabled ? "Monitoring on" : "Monitoring off")
+ if let lastMonitoredAt = trackedDomain.lastMonitoredAt {
+ Text("Checked \(lastMonitoredAt.formatted(date: .omitted, time: .shortened))")
+ }
+ if let lastAlertAt = trackedDomain.lastAlertAt {
+ Text("Alert \(lastAlertAt.formatted(date: .omitted, time: .shortened))")
+ }
+ }
+
+ private var domainTitle: some View {
+ HStack(alignment: .firstTextBaseline, spacing: 8) {
+ if trackedDomain.isPinned {
+ Image(systemName: "pin.fill")
+ .font(.caption2)
+ .foregroundStyle(Color(.statusWarning))
+ }
+ Text(trackedDomain.domain)
+ .font(appDensity.font(.callout))
+ .foregroundStyle(.primary)
+ .lineLimit(3)
+ .multilineTextAlignment(.leading)
+ .fixedSize(horizontal: false, vertical: true)
+ }
+ }
+
private func availabilityLabel(_ status: DomainAvailabilityStatus?) -> String {
switch status {
case .available:
diff --git a/DomainDigUITests/AccessibilityAuditHarness.swift b/DomainDigUITests/AccessibilityAuditHarness.swift
index 08ddedd..e025dcd 100644
--- a/DomainDigUITests/AccessibilityAuditHarness.swift
+++ b/DomainDigUITests/AccessibilityAuditHarness.swift
@@ -43,11 +43,20 @@ enum AccessibilityAuditHarness {
/// How many times to retry an audit that misses its internal deadline.
private static let auditAttempts = 3
+ /// Launch argument that seeds deterministic in-memory tracked domains and
+ /// batch results (DEBUG builds only; never persisted). Without it the dense
+ /// rows and portfolio sections render nothing, which is how five phases of
+ /// row treatment went unmeasured.
+ private static let seedFixturesArgument = "DOMAIN_DIG_SEED_FIXTURES"
+
/// Launches the app with feature gating lifted, optionally at a specific
- /// content size category.
- static func launch(contentSizeCategory: String? = nil) -> XCUIApplication {
+ /// content size category and with the audit fixtures seeded.
+ static func launch(contentSizeCategory: String? = nil, seeded: Bool = false) -> XCUIApplication {
let app = XCUIApplication()
app.launchArguments = [forceProPlusArgument]
+ if seeded {
+ app.launchArguments.append(seedFixturesArgument)
+ }
if let contentSizeCategory {
app.launchArguments += ["-UIPreferredContentSizeCategoryName", contentSizeCategory]
}
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 {