diff options
| author | Fuwn <[email protected]> | 2026-02-23 11:18:18 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-02-23 13:33:42 -0800 |
| commit | 6a1fd75d265c7e85c538cca07cbaea7e7da8fec7 (patch) | |
| tree | ebee4775b0a133b558429efc8edba11b445028eb | |
| parent | fix: guard post detail url actions against invalid urls (diff) | |
| download | sora-testing-6a1fd75d265c7e85c538cca07cbaea7e7da8fec7.tar.xz sora-testing-6a1fd75d265c7e85c538cca07cbaea7e7da8fec7.zip | |
feat: add explicit accessibility metadata for grid cells
| -rw-r--r-- | Sora/Views/FavoritesView.swift | 23 | ||||
| -rw-r--r-- | Sora/Views/Post/Grid/PostGridView.swift | 23 | ||||
| -rw-r--r-- | SoraTests/ViewDerivedDataTests.swift | 111 |
3 files changed, 147 insertions, 10 deletions
diff --git a/Sora/Views/FavoritesView.swift b/Sora/Views/FavoritesView.swift index a0357da..1217441 100644 --- a/Sora/Views/FavoritesView.swift +++ b/Sora/Views/FavoritesView.swift @@ -642,6 +642,29 @@ struct FavoritesView: View { // swiftlint:disable:this type_body_length Label("Delete", systemImage: "trash") } } + .accessibilityElement(children: .ignore) + .accessibilityLabel(Text(favoriteAccessibilityLabel(for: favorite))) + .accessibilityValue(Text(favoriteAccessibilityValue(for: favorite))) + .accessibilityHint(Text("Opens post details.")) + } + + private func favoriteAccessibilityLabel(for favorite: SettingsFavoritePost) -> String { + let tagSummary = favorite.tags + .prefix(3) + .map { tag in + tag.replacingOccurrences(of: "_", with: " ") + } + .joined(separator: ", ") + + if tagSummary.isEmpty { + return "Favorite post \(favorite.postId)" + } + + return tagSummary + } + + private func favoriteAccessibilityValue(for favorite: SettingsFavoritePost) -> String { + "Rating \(favorite.rating.rawValue.uppercased()). Provider \(favorite.provider.rawValue)." } } diff --git a/Sora/Views/Post/Grid/PostGridView.swift b/Sora/Views/Post/Grid/PostGridView.swift index 7a7b5fa..37c207e 100644 --- a/Sora/Views/Post/Grid/PostGridView.swift +++ b/Sora/Views/Post/Grid/PostGridView.swift @@ -514,6 +514,10 @@ struct PostGridView: View { // swiftlint:disable:this type_body_length Label("Add to Collection", systemImage: "folder.badge.plus") } } + .accessibilityElement(children: .ignore) + .accessibilityLabel(Text(postAccessibilityLabel(for: post))) + .accessibilityValue(Text(postAccessibilityValue(for: post))) + .accessibilityHint(Text("Opens post details.")) } private func isFavoritedInFolder(post: BooruPost, folderId: UUID) -> Bool { @@ -536,6 +540,25 @@ struct PostGridView: View { // swiftlint:disable:this type_body_length } } + private func postAccessibilityLabel(for post: BooruPost) -> String { + let tagSummary = post.tags + .prefix(3) + .map { tag in + tag.replacingOccurrences(of: "_", with: " ") + } + .joined(separator: ", ") + + if tagSummary.isEmpty { + return "Post \(post.id)" + } + + return tagSummary + } + + private func postAccessibilityValue(for post: BooruPost) -> String { + "Rating \(post.rating.rawValue.uppercased()). Score \(post.score)." + } + // MARK: - Local Search Methods private func performLocalSearch() async { let inputTags = localSearchText.components(separatedBy: .whitespaces).filter { component in diff --git a/SoraTests/ViewDerivedDataTests.swift b/SoraTests/ViewDerivedDataTests.swift index c20e9d1..7ec8f9f 100644 --- a/SoraTests/ViewDerivedDataTests.swift +++ b/SoraTests/ViewDerivedDataTests.swift @@ -245,6 +245,86 @@ final class ViewDerivedDataTests: XCTestCase { // swiftlint:disable:this type_b ) } + func testPostGridCellsProvideExplicitAccessibilityMetadata() throws { + let source = try loadSource(at: "Sora/Views/Post/Grid/PostGridView.swift") + let functionSource = try extractFunction( + named: "private func waterfallGridContent(post: BooruPost) -> some View", + from: source + ) + let normalizedSource = strippingCommentsAndStrings(from: functionSource) + let accessibilityLabelCount = tokenCount( + matching: #"\.accessibilityLabel\s*\("#, + in: normalizedSource + ) + let accessibilityHintCount = tokenCount( + matching: #"\.accessibilityHint\s*\("#, + in: normalizedSource + ) + let accessibilityValueCount = tokenCount( + matching: #"\.accessibilityValue\s*\("#, + in: normalizedSource + ) + + // swiftlint:disable:next prefer_nimble + XCTAssertGreaterThan( + accessibilityLabelCount, + 0, + "Post grid cells should expose an explicit accessibility label." + ) + // swiftlint:disable:next prefer_nimble + XCTAssertGreaterThan( + accessibilityHintCount, + 0, + "Post grid cells should expose an explicit accessibility hint." + ) + // swiftlint:disable:next prefer_nimble + XCTAssertGreaterThan( + accessibilityValueCount, + 0, + "Post grid cells should expose structured accessibility metadata." + ) + } + + func testFavoriteCellsProvideExplicitAccessibilityMetadata() throws { + let source = try loadSource(at: "Sora/Views/FavoritesView.swift") + let functionSource = try extractFunction( + named: "private func favoriteGridContent(", + from: source + ) + let normalizedSource = strippingCommentsAndStrings(from: functionSource) + let accessibilityLabelCount = tokenCount( + matching: #"\.accessibilityLabel\s*\("#, + in: normalizedSource + ) + let accessibilityHintCount = tokenCount( + matching: #"\.accessibilityHint\s*\("#, + in: normalizedSource + ) + let accessibilityValueCount = tokenCount( + matching: #"\.accessibilityValue\s*\("#, + in: normalizedSource + ) + + // swiftlint:disable:next prefer_nimble + XCTAssertGreaterThan( + accessibilityLabelCount, + 0, + "Favorite grid cells should expose an explicit accessibility label." + ) + // swiftlint:disable:next prefer_nimble + XCTAssertGreaterThan( + accessibilityHintCount, + 0, + "Favorite grid cells should expose an explicit accessibility hint." + ) + // swiftlint:disable:next prefer_nimble + XCTAssertGreaterThan( + accessibilityValueCount, + 0, + "Favorite grid cells should expose structured accessibility metadata." + ) + } + func testListViewsAvoidComparatorRandomShuffleSorting() throws { let listViewSource = try loadSource(at: "Sora/Views/Generic/GenericListView.swift") let favoritesViewSource = try loadSource(at: "Sora/Views/FavoritesView.swift") @@ -357,11 +437,14 @@ final class ViewDerivedDataTests: XCTestCase { // swiftlint:disable:this type_b from: source ) let danbooruCaseStart = try XCTUnwrap( - urlBuilderSection.range(of: "case .danbooru:")?.lowerBound) + urlBuilderSection.range(of: "case .danbooru:")?.lowerBound + ) let danbooruCaseEnd = try XCTUnwrap( urlBuilderSection.range( - of: "case .moebooru:", range: danbooruCaseStart..<urlBuilderSection.endIndex)? - .lowerBound + of: "case .moebooru:", + range: danbooruCaseStart..<urlBuilderSection.endIndex + )? + .lowerBound ) let danbooruSection = String(urlBuilderSection[danbooruCaseStart..<danbooruCaseEnd]) let danbooruLimitQueryCount = tokenCount( @@ -384,11 +467,14 @@ final class ViewDerivedDataTests: XCTestCase { // swiftlint:disable:this type_b from: source ) let moebooruCaseStart = try XCTUnwrap( - urlBuilderSection.range(of: "case .moebooru:")?.lowerBound) + urlBuilderSection.range(of: "case .moebooru:")?.lowerBound + ) let moebooruCaseEnd = try XCTUnwrap( urlBuilderSection.range( - of: "case .gelbooru:", range: moebooruCaseStart..<urlBuilderSection.endIndex)? - .lowerBound + of: "case .gelbooru:", + range: moebooruCaseStart..<urlBuilderSection.endIndex + )? + .lowerBound ) let moebooruSection = String(urlBuilderSection[moebooruCaseStart..<moebooruCaseEnd]) let moebooruTagHelperSection = try extractFunction( @@ -526,11 +612,14 @@ final class ViewDerivedDataTests: XCTestCase { // swiftlint:disable:this type_b from: source ) let danbooruCaseStart = try XCTUnwrap( - urlBuilderSection.range(of: "case .danbooru:")?.lowerBound) + urlBuilderSection.range(of: "case .danbooru:")?.lowerBound + ) let danbooruCaseEnd = try XCTUnwrap( urlBuilderSection.range( - of: "case .moebooru:", range: danbooruCaseStart..<urlBuilderSection.endIndex)? - .lowerBound + of: "case .moebooru:", + range: danbooruCaseStart..<urlBuilderSection.endIndex + )? + .lowerBound ) let danbooruSection = String(urlBuilderSection[danbooruCaseStart..<danbooruCaseEnd]) let pageTokenFunctionSection = try extractFunction( @@ -719,7 +808,9 @@ final class ViewDerivedDataTests: XCTestCase { // swiftlint:disable:this type_b let source = try loadSource(at: "Sora/Data/Danbooru/DanbooruPost.swift") let optionalFileURLCount = tokenCount(matching: #"let\s+fileURL:\s+String\?"#, in: source) let optionalLargeFileURLCount = tokenCount( - matching: #"let\s+largeFileURL:\s+String\?"#, in: source) + matching: #"let\s+largeFileURL:\s+String\?"#, + in: source + ) let optionalPreviewURLCount = tokenCount( matching: #"let\s+previewFileURL:\s+String\?"#, in: source |