From 40fb20bfb9b75756597feb5501537ed4e816ae8b Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Mon, 1 Sep 2025 18:05:46 +0200 Subject: [PATCH] accessibility improvements + tests refinement --- .../Sources/AccessibilityTests.swift | 16 ++++++++++++++-- ElementX/Sources/Other/MatrixEntityRegex.swift | 12 ++++++++++++ .../View/TimelineMediaPreviewDetailsView.swift | 1 + ...elineMediaPreviewRedactConfirmationView.swift | 1 + .../View/MediaEventsTimelineScreen.swift | 5 +++++ .../Screens/Spaces/Common/SpaceHeaderView.swift | 1 + 6 files changed, 34 insertions(+), 2 deletions(-) diff --git a/AccessibilityTests/Sources/AccessibilityTests.swift b/AccessibilityTests/Sources/AccessibilityTests.swift index 7092d203e..23cb4fcb7 100644 --- a/AccessibilityTests/Sources/AccessibilityTests.swift +++ b/AccessibilityTests/Sources/AccessibilityTests.swift @@ -16,7 +16,7 @@ final class AccessibilityTests: XCTestCase { app = Application.launch(viewID: name) await client.waitForApp() defer { try? client.stop() } - + try client.send(.accessibilityAudit(.nextPreview)) // To handle location sharing popup in CI @@ -55,6 +55,11 @@ final class AccessibilityTests: XCTestCase { do { // We have removed `textClipped` and `contrast` for now try app.performAccessibilityAudit(for: [.dynamicType, .elementDetection, .hitRegion, .sufficientElementDescription, .trait]) { issue in + // Specific tests do not need specific accessibilty audit types + if Self.ignoredA11yTest[name]?.isAccessibilityIssueFiltered(issue) == true { + return true + } + // Remove false positives for null elements guard let element = issue.element else { return true @@ -84,16 +89,23 @@ final class AccessibilityTests: XCTestCase { } private static func isMatrixIdentifier(_ string: String) -> Bool { - MatrixEntityRegex.isMatrixRoomAlias(string) || MatrixEntityRegex.isMatrixUserIdentifier(string) || string == PillUtilities.atRoom + MatrixEntityRegex.isMatrixRoomAlias(string) || MatrixEntityRegex.isMatrixUserIdentifier(string) || string == PillUtilities.atRoom || MatrixEntityRegex.isLegacyMatrixRoomID(string) } private static let partiallyUnsupportedDynamicTypeMessage = "Dynamic Type font sizes are partially unsupported" private static let notHumanReadableMessage = "Label not human-readable" + private static let elementHasNoDescription = "Element has no description" /// Use this array to filter add specific filters to ignore specific issues for certain elements private static let ignoredA11yIdentifiers: [String: [FilterType]] = [ A11yIdentifiers.serverConfirmationScreen.serverPicker: [.compactDescription(notHumanReadableMessage)] ] + + /// Use this array to filter add specific filters to ignore specific tests + private static let ignoredA11yTest: [String: [FilterType]] = [ + // It's an image rendering test doesn't need to have descriptions + "RoomAvatarImage_Previews-0": [.auditType(.sufficientElementDescription)] + ] } private enum FilterType { diff --git a/ElementX/Sources/Other/MatrixEntityRegex.swift b/ElementX/Sources/Other/MatrixEntityRegex.swift index 66fdf53f8..d9d5ddf7c 100644 --- a/ElementX/Sources/Other/MatrixEntityRegex.swift +++ b/ElementX/Sources/Other/MatrixEntityRegex.swift @@ -14,6 +14,7 @@ enum MatrixEntityRegex: String { case roomAlias case uri case allUsers + case legacyRoomID var rawValue: String { switch self { @@ -23,6 +24,8 @@ enum MatrixEntityRegex: String { return "@[\\x21-\\x39\\x3B-\\x7F]+:" + MatrixEntityRegex.homeserver.rawValue case .roomAlias: return "#[A-Z0-9._%#@=+-]+:" + MatrixEntityRegex.homeserver.rawValue + case .legacyRoomID: + return "![A-Z0-9_\\-\\/]+:" + MatrixEntityRegex.homeserver.rawValue case .uri: return "matrix:(r|u|roomid)\\/[A-Z0-9\\-._~:/?#\\[\\]@!$&'()*+,;=%]*(?:\\?[A-Z0-9\\-._~:/?#\\[\\]@!$&'()*+,;=%]*)?" case .allUsers: @@ -37,6 +40,7 @@ enum MatrixEntityRegex: String { static let uriRegex = try! NSRegularExpression(pattern: MatrixEntityRegex.uri.rawValue, options: .caseInsensitive) static let allUsersRegex = try! NSRegularExpression(pattern: MatrixEntityRegex.allUsers.rawValue) static let linkRegex = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) + static let legacyRoomIDRegex = try! NSRegularExpression(pattern: MatrixEntityRegex.legacyRoomID.rawValue, options: .caseInsensitive) // swiftlint:enable force_try static func isMatrixHomeserver(_ homeserver: String) -> Bool { @@ -63,6 +67,14 @@ enum MatrixEntityRegex: String { return match.range.length == alias.count } + static func isLegacyMatrixRoomID(_ roomID: String) -> Bool { + guard let match = legacyRoomIDRegex.firstMatch(in: roomID) else { + return false + } + + return match.range.length == roomID.count + } + static func isMatrixURI(_ uri: String) -> Bool { guard let match = uriRegex.firstMatch(in: uri) else { return false diff --git a/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewDetailsView.swift b/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewDetailsView.swift index 517915a69..f10b8f97b 100644 --- a/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewDetailsView.swift +++ b/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewDetailsView.swift @@ -44,6 +44,7 @@ struct TimelineMediaPreviewDetailsView: View { contentID: item.sender.id, avatarSize: .user(on: .mediaPreviewDetails), mediaProvider: context.mediaProvider) + .accessibilityHidden(true) VStack(alignment: .leading, spacing: 0) { if let displayName = item.sender.displayName { diff --git a/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewRedactConfirmationView.swift b/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewRedactConfirmationView.swift index 3a405800b..e4b7769ad 100644 --- a/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewRedactConfirmationView.swift +++ b/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewRedactConfirmationView.swift @@ -71,6 +71,7 @@ struct TimelineMediaPreviewRedactConfirmationView: View { .aspectRatio(contentMode: .fill) } .clipShape(RoundedRectangle(cornerRadius: 8)) + .accessibilityHidden(true) } VStack(alignment: .leading, spacing: 4) { diff --git a/ElementX/Sources/Screens/MediaEventsTimelineScreen/View/MediaEventsTimelineScreen.swift b/ElementX/Sources/Screens/MediaEventsTimelineScreen/View/MediaEventsTimelineScreen.swift index 0919bacdf..9ebac7254 100644 --- a/ElementX/Sources/Screens/MediaEventsTimelineScreen/View/MediaEventsTimelineScreen.swift +++ b/ElementX/Sources/Screens/MediaEventsTimelineScreen/View/MediaEventsTimelineScreen.swift @@ -85,6 +85,7 @@ struct MediaEventsTimelineScreen: View { ForEach(group.items) { item in VStack(spacing: 20) { Divider() + .accessibilityHidden(true) Button { tappedItem(item) @@ -92,7 +93,11 @@ struct MediaEventsTimelineScreen: View { viewForTimelineItem(item) .scaleEffect(CGSize(width: 1, height: -1)) } + .accessibilityRepresentation { + viewForTimelineItem(item) + } } + .accessibilityElement(children: .combine) .padding(.horizontal, 16) } } footer: { diff --git a/ElementX/Sources/Screens/Spaces/Common/SpaceHeaderView.swift b/ElementX/Sources/Screens/Spaces/Common/SpaceHeaderView.swift index 19f87401e..fad316a57 100644 --- a/ElementX/Sources/Screens/Spaces/Common/SpaceHeaderView.swift +++ b/ElementX/Sources/Screens/Spaces/Common/SpaceHeaderView.swift @@ -19,6 +19,7 @@ struct SpaceHeaderView: View { RoomAvatarImage(avatar: spaceRoomProxy.avatar, avatarSize: .room(on: .spaceHeader), mediaProvider: mediaProvider) + .accessibilityHidden(true) VStack(spacing: 8) { Text(title)