// swiftlint:disable file_length import XCTest final class ViewDerivedDataTests: XCTestCase { // swiftlint:disable:this type_body_length func testGenericListViewDerivedCollectionsAreReferencedOncePerRenderPass() throws { let source = try loadSource(at: "Sora/Views/Generic/GenericListView.swift") let normalizedSource = strippingCommentsAndStrings(from: source) let filteredItemsUsages = referenceCount( for: "filteredItems", in: normalizedSource ) let sortedFilteredItemsUsages = referenceCount( for: "sortedFilteredItems", in: normalizedSource ) // swiftlint:disable:next prefer_nimble XCTAssertLessThanOrEqual( filteredItemsUsages, 1, "filteredItems should be consumed once per dependency change." ) // swiftlint:disable:next prefer_nimble XCTAssertLessThanOrEqual( sortedFilteredItemsUsages, 1, "sortedFilteredItems should be consumed once per dependency change." ) } func testPostGridViewDerivedCollectionsAreReferencedOncePerRenderPass() throws { let source = try loadSource(at: "Sora/Views/Post/Grid/PostGridView.swift") let normalizedSource = strippingCommentsAndStrings(from: source) let activePostsUsages = referenceCount( for: "activePosts", in: normalizedSource ) let getColumnsDataUsages = invocationCount( forFunction: "getColumnsData", in: normalizedSource ) // swiftlint:disable:next prefer_nimble XCTAssertLessThanOrEqual( activePostsUsages, 1, "activePosts-derived data should be consumed once per dependency change." ) // swiftlint:disable:next prefer_nimble XCTAssertLessThanOrEqual( getColumnsDataUsages, 1, "getColumnsData should be invoked once per dependency change." ) } func testFavoritesViewDerivedCollectionsAreReferencedOncePerRenderPass() throws { let source = try loadSource(at: "Sora/Views/FavoritesView.swift") let normalizedSource = strippingCommentsAndStrings(from: source) let filteredFavoritesUsages = referenceCount( for: "filteredFavorites", in: normalizedSource ) let sortedFilteredFavoritesUsages = referenceCount( for: "sortedFilteredFavorites", in: normalizedSource ) let inlinePostMappingCount = tokenCount( matching: #"\bsortedFilteredFavorites\s*\.\s*map\s*\{"#, in: normalizedSource ) let columnsSplitterUsages = invocationCount( forFunction: "getColumnsData", in: normalizedSource ) // swiftlint:disable:next prefer_nimble XCTAssertLessThanOrEqual( filteredFavoritesUsages, 1, "filteredFavorites should be consumed once per dependency change." ) // swiftlint:disable:next prefer_nimble XCTAssertLessThanOrEqual( sortedFilteredFavoritesUsages, 1, "sortedFilteredFavorites should be consumed once per dependency change." ) // swiftlint:disable:next prefer_nimble XCTAssertEqual( inlinePostMappingCount, 0, "Favorites grid should not remap sorted favorites to posts inline on each cell render." ) // swiftlint:disable:next prefer_nimble XCTAssertLessThanOrEqual( columnsSplitterUsages, 1, "Favorites grid column distribution should be derived once per dependency change." ) } func testFolderMenuHierarchyIsSharedAcrossConsumers() throws { let consumerPaths = [ "Sora/Views/Generic/GenericListView.swift", "Sora/Views/FavoritesView.swift", "Sora/Views/Post/Grid/PostGridView.swift", "Sora/Views/BookmarkMenuButtonView.swift", "Sora/Views/FavoriteMenuButtonView.swift", ] for consumerPath in consumerPaths { let source = try loadSource(at: consumerPath) let sharedMenuUsages = tokenCount( matching: #"\bFolderMenuView\s*\("#, in: source ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( sharedMenuUsages, 0, "\(consumerPath) should use FolderMenuView for folder hierarchy rendering." ) } } func testSearchSuggestionsCacheIsBuiltFromItemsChanges() throws { let source = try loadSource(at: "Sora/Views/SearchSuggestionsView.swift") let normalizedSource = strippingCommentsAndStrings(from: source) let stateCacheCount = tokenCount( matching: #"\@State\s+private\s+var\s+cachedTags"#, in: normalizedSource ) let itemsMapCount = tokenCount( matching: #"\bcachedTags\s*=\s*items\s*\.\s*map"#, in: normalizedSource ) let itemsChangeObserverCount = tokenCount( matching: #"\.onChange\s*\(\s*of:\s*itemsCacheKey\s*\)"#, in: normalizedSource ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( stateCacheCount, 0, "Search suggestions should keep preprocessed tags in view state." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( itemsMapCount, 0, "Search suggestions should build cached tags from items." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( itemsChangeObserverCount, 0, "Search suggestions should refresh cached tags only when items change." ) } func testPostDetailsImageActionsUseAsyncCachedImageLoading() throws { let source = try loadSource(at: "Sora/Views/Post/Details/PostDetailsImageView.swift") let normalizedSource = strippingCommentsAndStrings(from: source) let synchronousLoadCount = tokenCount( matching: #"\bNSData\s*\(\s*contentsOf:"#, in: normalizedSource ) let cachedLoaderCount = tokenCount( matching: #"\bImageCacheManager\s*\.\s*shared\s*\.\s*loadImageData\s*\("#, in: normalizedSource ) // swiftlint:disable:next prefer_nimble XCTAssertEqual( synchronousLoadCount, 0, "Post details image actions should avoid synchronous NSData file or network loads." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( cachedLoaderCount, 0, "Post details image actions should use cache-backed async image loading." ) } func testPostDetailsImageViewAvoidsForceUnwrappedRuntimeURLs() throws { let source = try loadSource(at: "Sora/Views/Post/Details/PostDetailsImageView.swift") let normalizedSource = strippingCommentsAndStrings(from: source) let forcedShareFallbackURLCount = tokenCount( matching: #"\burl\s*\?\?\s*URL\s*\(\s*string:\s*\)\s*!"#, in: normalizedSource ) let forcedSourceURLCount = tokenCount( matching: #"\bURL\s*\(\s*string:\s*source\s*\)\s*!"#, in: normalizedSource ) let forcedPostURLBuilderCount = tokenCount( matching: #"\breturn\s+URL\s*\(\s*string:\s*[^)]+\)\s*!"#, in: normalizedSource ) // swiftlint:disable:next prefer_nimble XCTAssertEqual( forcedShareFallbackURLCount, 0, "Post details share actions should not force unwrap fallback URLs." ) // swiftlint:disable:next prefer_nimble XCTAssertEqual( forcedSourceURLCount, 0, "Post details source links should be validated before opening." ) // swiftlint:disable:next prefer_nimble XCTAssertEqual( forcedPostURLBuilderCount, 0, "Post details post-url helpers should return optional URLs instead of force-unwrapping." ) } func testPostDetailsViewAvoidsForceUnwrappedShareURL() throws { let source = try loadSource(at: "Sora/Views/Post/Details/PostDetailsView.swift") let normalizedSource = strippingCommentsAndStrings(from: source) let forcedShareItemCount = tokenCount( matching: #"\bShareLink\s*\(\s*item:\s*imageURL\s*!"#, in: normalizedSource ) // swiftlint:disable:next prefer_nimble XCTAssertEqual( forcedShareItemCount, 0, "Post details share actions should not force unwrap image URLs." ) } 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 testInteractiveImageViewProvidesExplicitAccessibilityMetadata() throws { let source = try loadSource(at: "Sora/Views/InteractiveImageView.swift") let normalizedSource = strippingCommentsAndStrings(from: source) 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, "Interactive image view should expose an explicit accessibility label." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( accessibilityHintCount, 0, "Interactive image view should expose an explicit accessibility hint." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( accessibilityValueCount, 0, "Interactive image view should expose stateful accessibility metadata." ) } func testFavoritesAccessibilityFallbackAndValueUseLocalizedHumanStrings() throws { let source = try loadSource(at: "Sora/Views/FavoritesView.swift") let labelFunctionSource = try extractFunction( named: "private func favoriteAccessibilityLabel(", from: source ) let valueFunctionSource = try extractFunction( named: "private func favoriteAccessibilityValue(", from: source ) let fallbackLabelLocalizationCount = tokenCount( matching: #"String\(localized:\s*"Favorite post"#, in: labelFunctionSource ) let accessibilityValueLocalizationCount = tokenCount( matching: #"String\(localized:\s*"Rating"#, in: valueFunctionSource ) let uppercasedRatingCount = tokenCount( matching: #"favorite\.rating\.rawValue\.uppercased\(\)"#, in: valueFunctionSource ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( fallbackLabelLocalizationCount, 0, "Favorite accessibility fallback label should use localized copy." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( accessibilityValueLocalizationCount, 0, "Favorite accessibility value should use localized copy." ) // swiftlint:disable:next prefer_nimble XCTAssertEqual( uppercasedRatingCount, 0, "Favorite accessibility value should not uppercase rating text." ) } func testPostGridAccessibilityFallbackAndValueUseLocalizedHumanStrings() throws { let source = try loadSource(at: "Sora/Views/Post/Grid/PostGridView.swift") let labelFunctionSource = try extractFunction( named: "private func postAccessibilityLabel(", from: source ) let valueFunctionSource = try extractFunction( named: "private func postAccessibilityValue(", from: source ) let fallbackLabelLocalizationCount = tokenCount( matching: #"String\(localized:\s*"Post"#, in: labelFunctionSource ) let accessibilityValueLocalizationCount = tokenCount( matching: #"String\(localized:\s*"Rating"#, in: valueFunctionSource ) let uppercasedRatingCount = tokenCount( matching: #"post\.rating\.rawValue\.uppercased\(\)"#, in: valueFunctionSource ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( fallbackLabelLocalizationCount, 0, "Post accessibility fallback label should use localized copy." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( accessibilityValueLocalizationCount, 0, "Post accessibility value should use localized copy." ) // swiftlint:disable:next prefer_nimble XCTAssertEqual( uppercasedRatingCount, 0, "Post accessibility value should not uppercase rating text." ) } func testFavoritesDeleteActionUsesDestructiveRole() throws { let source = try loadSource(at: "Sora/Views/FavoritesView.swift") let functionSource = try extractFunction( named: "private func favoriteGridContent(", from: source ) let destructiveDeleteActionCount = tokenCount( matching: #"role:\s*\.destructive[\s\S]*Label\(\s*"Delete"\s*,\s*systemImage:\s*"trash"\s*\)"#, in: functionSource ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( destructiveDeleteActionCount, 0, "Favorite context menu delete action should be explicitly destructive." ) } func testGenericListDeleteActionUsesDestructiveRole() throws { let source = try loadSource(at: "Sora/Views/Generic/GenericListView.swift") let destructiveDeleteActionCount = tokenCount( matching: #"role:\s*\.destructive[\s\S]*Label\(\s*"Delete"\s*,\s*systemImage:\s*"trash"\s*\)"#, in: source ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( destructiveDeleteActionCount, 0, "Generic list context menu delete action should be explicitly destructive." ) } func testFavoritesRemoveAllAlertUsesDestructiveRole() throws { let source = try loadSource(at: "Sora/Views/FavoritesView.swift") let destructiveRemoveAllCount = tokenCount( matching: #"Button\("Remove All Favorites",\s*role:\s*\.destructive\)"#, in: source ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( destructiveRemoveAllCount, 0, "Favorites remove-all confirmation should mark the destructive action with role." ) } func testGenericListRemoveAllAlertUsesDestructiveRole() throws { let source = try loadSource(at: "Sora/Views/Generic/GenericListView.swift") let destructiveRemoveAllCount = tokenCount( matching: #"Button\(removeAllButtonText,\s*role:\s*\.destructive\)"#, in: source ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( destructiveRemoveAllCount, 0, "Generic list remove-all confirmation should mark the destructive action with role." ) } func testListViewsAvoidComparatorRandomShuffleSorting() throws { let listViewSource = try loadSource(at: "Sora/Views/Generic/GenericListView.swift") let favoritesViewSource = try loadSource(at: "Sora/Views/FavoritesView.swift") let normalizedListViewSource = strippingCommentsAndStrings(from: listViewSource) let normalizedFavoritesViewSource = strippingCommentsAndStrings(from: favoritesViewSource) let listViewComparatorRandomCount = tokenCount( matching: #"\bBool\s*\.\s*random\s*\("#, in: normalizedListViewSource ) let favoritesViewComparatorRandomCount = tokenCount( matching: #"\bBool\s*\.\s*random\s*\("#, in: normalizedFavoritesViewSource ) // swiftlint:disable:next prefer_nimble XCTAssertEqual( listViewComparatorRandomCount, 0, "Generic list sorting should not use comparator-based random ordering." ) // swiftlint:disable:next prefer_nimble XCTAssertEqual( favoritesViewComparatorRandomCount, 0, "Favorites sorting should not use comparator-based random ordering." ) } func testBooruManagerRemovesUnusedXMLParserPoolPaths() throws { let source = try loadSource(at: "Sora/Data/Booru/BooruManager.swift") let normalizedSource = strippingCommentsAndStrings(from: source) let poolStorageCount = tokenCount( matching: #"\bxmlParserPool\b"#, in: normalizedSource ) let lockStorageCount = tokenCount( matching: #"\bparserPoolLock\b"#, in: normalizedSource ) let parserFactoryCount = invocationCount( forFunction: "xmlParser", in: normalizedSource ) let parserReturnCount = invocationCount( forFunction: "returnXMLParser", in: normalizedSource ) // swiftlint:disable:next prefer_nimble XCTAssertEqual( poolStorageCount, 0, "BooruManager should not keep an unused XML parser pool." ) // swiftlint:disable:next prefer_nimble XCTAssertEqual( lockStorageCount, 0, "BooruManager should not keep an unused parser pool lock." ) // swiftlint:disable:next prefer_nimble XCTAssertEqual( parserFactoryCount, 0, "BooruManager should not expose unused xml parser factory paths." ) // swiftlint:disable:next prefer_nimble XCTAssertEqual( parserReturnCount, 0, "BooruManager should not expose unused xml parser return paths." ) } func testBooruManagerDanbooruPostsUseQueryParamAuthentication() throws { let source = try loadSource(at: "Sora/Data/Booru/BooruManager.swift") let urlBuilderSection = try extractFunction( named: "func url(forPosts page: Int, limit: Int, tags: [String]) -> URL?", from: source ) let danbooruLoginQueryCount = tokenCount( matching: #"case\s+\.danbooru[\s\S]*URLQueryItem\(name:\s*"login""#, in: urlBuilderSection ) let danbooruAPIKeyQueryCount = tokenCount( matching: #"case\s+\.danbooru[\s\S]*URLQueryItem\(name:\s*"api_key""#, in: urlBuilderSection ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( danbooruLoginQueryCount, 0, "Danbooru requests should authenticate with a `login` query parameter." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( danbooruAPIKeyQueryCount, 0, "Danbooru requests should authenticate with an `api_key` query parameter." ) } func testBooruManagerDanbooruPostsIncludeLimitParameter() throws { let source = try loadSource(at: "Sora/Data/Booru/BooruManager.swift") let urlBuilderSection = try extractFunction( named: "func url(forPosts page: Int, limit: Int, tags: [String]) -> URL?", from: source ) let danbooruCaseStart = try XCTUnwrap( urlBuilderSection.range(of: "case .danbooru:")?.lowerBound ) let danbooruCaseEnd = try XCTUnwrap( urlBuilderSection.range( of: "case .moebooru:", range: danbooruCaseStart.. URL?", from: source ) let moebooruCaseStart = try XCTUnwrap( urlBuilderSection.range(of: "case .moebooru:")?.lowerBound ) let moebooruCaseEnd = try XCTUnwrap( urlBuilderSection.range( of: "case .gelbooru:", range: moebooruCaseStart.. String", from: source ) let moebooruTagHelperUsageCount = tokenCount( matching: #"let\s+moebooruTags\s*=\s*moebooruTagString\(for:\s*tags\)"#, in: moebooruSection ) let holdsFalseDefaultCount = tokenCount( matching: #"\(tags\s*\+\s*\["holds:false"\]\)\.joined\(separator:\s*"\+"\)"#, in: moebooruTagHelperSection ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( moebooruTagHelperUsageCount, 0, "Moebooru requests should derive tags through a helper that can enforce feed-visibility filters." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( holdsFalseDefaultCount, 0, "Moebooru requests should default to `holds:false` to match website feed visibility." ) } func testBooruManagerMoebooruRespectsExplicitHoldsTag() throws { let source = try loadSource(at: "Sora/Data/Booru/BooruManager.swift") let moebooruTagHelperSection = try extractFunction( named: "private func moebooruTagString(for tags: [String]) -> String", from: source ) let explicitHoldsDetectionCount = tokenCount( matching: #"hasPrefix\(\"holds:\"\)"#, in: moebooruTagHelperSection ) let explicitHoldsBypassCount = tokenCount( matching: #"if\s+hasExplicitHoldsFilter\s*\{\s*return\s+tags\.joined\(separator:\s*"\+"\)\s*\}"#, in: moebooruTagHelperSection ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( explicitHoldsDetectionCount, 0, "Moebooru tag helper should detect explicit holds filters in search tags." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( explicitHoldsBypassCount, 0, "Moebooru tag helper should not override explicit user-supplied `holds:*` filters." ) } func testSettingsManagerPersistsShowHeldMoebooruPostsFlag() throws { let source = try loadSource(at: "Sora/Data/Settings/SettingsManager.swift") let showHeldSettingCount = tokenCount( matching: #"\@AppStorage\("showHeldMoebooruPosts"\)\s*var\s+showHeldMoebooruPosts\s*=\s*false"#, in: source ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( showHeldSettingCount, 0, "SettingsManager should persist a `showHeldMoebooruPosts` toggle with a default of false." ) } func testMainViewPassesShowHeldMoebooruPostsToBooruManager() throws { let source = try loadSource(at: "Sora/Views/MainView.swift") let managerShowHeldWiringCount = tokenCount( matching: #"showHeldMoebooruPosts:\s*settings\.showHeldMoebooruPosts"#, in: source ) let showHeldChangeObserverCount = tokenCount( matching: #"\.onChange\s*\(\s*of:\s*settings\.showHeldMoebooruPosts\s*\)"#, in: source ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( managerShowHeldWiringCount, 1, "MainView should wire held-post toggle into all BooruManager reconstructions." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( showHeldChangeObserverCount, 0, "MainView should refresh BooruManager when held-post visibility setting changes." ) } func testBooruManagerSupportsShowHeldMoebooruPostsMode() throws { let source = try loadSource(at: "Sora/Data/Booru/BooruManager.swift") let initSignatureCount = tokenCount( matching: #"showHeldMoebooruPosts:\s*Bool\s*=\s*false"#, in: source ) let moebooruTagHelperSection = try extractFunction( named: "private func moebooruTagString(for tags: [String]) -> String", from: source ) let showHeldBypassCount = tokenCount( matching: #"guard\s*!\s*showHeldMoebooruPosts\s*else\s*\{\s*return\s+tags\.joined\(separator:\s*"\+"\)\s*\}"#, in: moebooruTagHelperSection ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( initSignatureCount, 0, "BooruManager should expose a `showHeldMoebooruPosts` initialization option." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( showHeldBypassCount, 0, "Moebooru tag helper should bypass forced `holds:false` when showing held posts is enabled." ) } func testBooruManagerDanbooruPostsUseBeforeCursorPagination() throws { let source = try loadSource(at: "Sora/Data/Booru/BooruManager.swift") let urlBuilderSection = try extractFunction( named: "func url(forPosts page: Int, limit: Int, tags: [String]) -> URL?", from: source ) let danbooruCaseStart = try XCTUnwrap( urlBuilderSection.range(of: "case .danbooru:")?.lowerBound ) let danbooruCaseEnd = try XCTUnwrap( urlBuilderSection.range( of: "case .moebooru:", range: danbooruCaseStart.. String", from: source ) let danbooruCursorPageQueryCount = tokenCount( matching: #"URLQueryItem\(name:\s*"page",\s*value:\s*danbooruPageToken\(for:\s*page,\s*tags:\s*tags\)\)"#, in: danbooruSection ) let beforeCursorCount = tokenCount( matching: #""b\\\#\("#, in: pageTokenFunctionSection ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( danbooruCursorPageQueryCount, 0, "Danbooru requests should derive the `page` query using a cursor-aware token helper." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( beforeCursorCount, 0, "Danbooru cursor pagination should build `page=b` for subsequent pages." ) } func testBooruManagerDanbooruPaginationFallsBackToNumericPageForExplicitSort() throws { let source = try loadSource(at: "Sora/Data/Booru/BooruManager.swift") let pageTokenFunctionSection = try extractFunction( named: "private func danbooruPageToken(for page: Int, tags: [String]) -> String", from: source ) let explicitSortGuardCount = tokenCount( matching: #"guard\s*!\s*hasExplicitSortTag\(in:\s*tags\)\s*else\s*\{\s*return\s*String\(page\)\s*\}"#, in: pageTokenFunctionSection ) let sortTagHelperSection = try extractFunction( named: "private func hasExplicitSortTag(in tags: [String]) -> Bool", from: source ) let sortTagDetectionCount = tokenCount( matching: #"hasPrefix\("order:"\)"#, in: sortTagHelperSection ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( explicitSortGuardCount, 0, "Danbooru pagination should fallback to numeric pages for explicit `order:*` searches." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( sortTagDetectionCount, 0, "Danbooru pagination should detect explicit sort tags from search input." ) } func testBooruManagerDanbooruPaginationCachesMinimumPostIDForCursorToken() throws { let source = try loadSource(at: "Sora/Data/Booru/BooruManager.swift") let pageTokenFunctionSection = try extractFunction( named: "private func danbooruPageToken(for page: Int, tags: [String]) -> String", from: source ) let updatePostsSection = try extractFunction( named: "private func updatePosts(_ newPosts: [BooruPost], replace: Bool)", from: source ) let cachedMinimumPostIDStorageCount = tokenCount( matching: #"private\s+var\s+cachedMinimumPostID:\s*Int\?"#, in: source ) let cachedTokenGuardCount = tokenCount( matching: #"guard\s+let\s+minimumPostID\s*=\s*cachedMinimumPostID"#, in: pageTokenFunctionSection ) let directPostsMinScanCount = tokenCount( matching: #"posts\s*\.\s*lazy\s*\.\s*compactMap\s*\(\s*\{\s*Int\(\$0\.id\)\s*\}\s*\)\s*\.\s*min"#, in: pageTokenFunctionSection ) let cacheInvalidationOnReplaceCount = tokenCount( matching: #"cachedMinimumPostID\s*=\s*nil"#, in: updatePostsSection ) let cacheRefreshCount = tokenCount( matching: #"cachedMinimumPostID\s*=\s*min\("#, in: updatePostsSection ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( cachedMinimumPostIDStorageCount, 0, "BooruManager should store a cached minimum post ID for Danbooru pagination." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( cachedTokenGuardCount, 0, "Danbooru page token generation should use cached minimum post ID." ) // swiftlint:disable:next prefer_nimble XCTAssertEqual( directPostsMinScanCount, 0, "Danbooru page token generation should avoid rescanning all posts for minimum ID." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( cacheInvalidationOnReplaceCount, 0, "Replacing posts should invalidate cached Danbooru minimum post ID." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( cacheRefreshCount, 0, "Appending posts should refresh cached Danbooru minimum post ID incrementally." ) } func testBooruManagerDoesNotForceClientSidePostResort() throws { let source = try loadSource(at: "Sora/Data/Booru/BooruManager.swift") let retrySection = try extractFunction( named: "private func fetchPostsWithRetry(url: URL) async -> [BooruPost]", from: source ) let forcedResortCount = tokenCount( matching: #"\.sorted\s*\{\s*\$0\.createdAt\s*>\s*\$1\.createdAt\s*\}"#, in: retrySection ) // swiftlint:disable:next prefer_nimble XCTAssertEqual( forcedResortCount, 0, "Fetched posts should preserve provider API ordering and not be force-sorted by created_at." ) } func testBooruManagerParsePostsPreservesAPIOrderWhenDeduplicating() throws { let source = try loadSource(at: "Sora/Data/Booru/BooruManager.swift") let parsePostsSection = try extractFunction( named: "nonisolated static func parsePosts(", from: source ) let stableDedupeInsertCount = tokenCount( matching: #"seenPostIDs\.insert\(post\.id\)\.inserted"#, in: parsePostsSection ) let orderedAppendCount = tokenCount( matching: #"orderedUniquePosts\.append\(post\)"#, in: parsePostsSection ) let orderedReturnCount = tokenCount( matching: #"return\s+orderedUniquePosts"#, in: parsePostsSection ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( stableDedupeInsertCount, 0, "Post dedup should track seen IDs without scrambling API order." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( orderedAppendCount, 0, "Post dedup should append in parser order to preserve provider ranking." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( orderedReturnCount, 0, "Post parser output should return the stable ordered array after deduplication." ) } func testBooruManagerValidatesHTTPStatusCodesForRequests() throws { let source = try loadSource(at: "Sora/Data/Booru/BooruManager.swift") let requestURLSection = try extractFunction( named: "func requestURL(_ url: URL) async throws -> Data", from: source ) let statusValidationCount = tokenCount( matching: #"\.validate\(statusCode:\s*200\.\.<300\)"#, in: requestURLSection ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( statusValidationCount, 0, "BooruManager should validate HTTP status codes to avoid silent API failures." ) } func testBooruManagerDanbooruFallsBackToUnauthenticatedRequestsOnUnauthorized() throws { let source = try loadSource(at: "Sora/Data/Booru/BooruManager.swift") let retrySection = try extractFunction( named: "private func fetchPostsWithRetry(url: URL) async -> [BooruPost]", from: source ) let credentialStripperSection = try extractFunction( named: "private static func removingDanbooruCredentials(from url: URL) -> URL?", from: source ) let unauthorizedResponseCheckCount = tokenCount( matching: #"responseCode\s*==\s*401"#, in: retrySection ) let fallbackInvocationCount = tokenCount( matching: #"removingDanbooruCredentials\(from:\s*requestURL\)"#, in: retrySection ) let strippedQueryItemCount = tokenCount( matching: #"queryItem\.name\s*!=\s*"login"\s*&&\s*queryItem\.name\s*!=\s*"api_key""#, in: credentialStripperSection ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( unauthorizedResponseCheckCount, 0, "Danbooru fetch retries should detect unauthorized responses." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( fallbackInvocationCount, 0, "Danbooru fetch retries should retry without credentials after unauthorized responses." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( strippedQueryItemCount, 0, "Credential fallback should remove both `login` and `api_key` query parameters." ) } func testDanbooruPostModelUsesOptionalVisibilityDependentFields() throws { 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 ) let optionalPreviewURLCount = tokenCount( matching: #"let\s+previewFileURL:\s+String\?"#, in: source ) let optionalMD5Count = tokenCount(matching: #"let\s+md5:\s+String\?"#, in: source) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( optionalFileURLCount, 0, "Danbooru model should decode visibility-dependent file URLs as optional." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( optionalLargeFileURLCount, 0, "Danbooru model should decode visibility-dependent large file URLs as optional." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( optionalPreviewURLCount, 0, "Danbooru model should decode visibility-dependent preview URLs as optional." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( optionalMD5Count, 0, "Danbooru model should decode visibility-dependent MD5 values as optional." ) } func testDanbooruParserUsesLossyDecodingForMixedRecordValidity() throws { let source = try loadSource(at: "Sora/Data/Danbooru/DanbooruPostParser.swift") let lossyDecoderUsageCount = tokenCount( matching: #"\[FailableDecodable\]\.self"#, in: source ) let malformedRecordDebugCount = tokenCount( matching: #"dropped\s*\\\#\(droppedRecordCount\)\s*malformed records"#, in: source ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( lossyDecoderUsageCount, 0, "Danbooru parser should decode lossily so one malformed item doesn't drop the entire page." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( malformedRecordDebugCount, 0, "Danbooru parser should log dropped malformed records for observability." ) } func testThumbnailGridViewAvoidsDirectColumnArrayIndexing() throws { let source = try loadSource(at: "Sora/Views/Shared/ThumbnailGridView.swift") let normalizedSource = strippingCommentsAndStrings(from: source) let directColumnIndexingCount = tokenCount( matching: #"\bcolumnsData\s*\[\s*columnIndex\s*\]"#, in: normalizedSource ) // swiftlint:disable:next prefer_nimble XCTAssertEqual( directColumnIndexingCount, 0, "Thumbnail grid should avoid direct column indexing that can crash when column and data counts drift." ) } private func referenceCount(for symbol: String, in source: String) -> Int { let totalMatches = tokenCount(matching: #"\b\#(symbol)\b"#, in: source) let declarationMatches = tokenCount(matching: #"\b(?:var|let)\s+\#(symbol)\b"#, in: source) return max(0, totalMatches - declarationMatches) } private func invocationCount(forFunction name: String, in source: String) -> Int { let totalMatches = tokenCount(matching: #"\b\#(name)\s*\("#, in: source) let declarationMatches = tokenCount(matching: #"\bfunc\s+\#(name)\s*\("#, in: source) return max(0, totalMatches - declarationMatches) } }