// 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 testSearchSuggestionsViewDerivesCachedTagsDirectlyFromItems() throws { let source = try loadSource(at: "Sora/Views/SearchSuggestionsView.swift") let normalizedSource = strippingCommentsAndStrings(from: source) let cachedTagsSection = try extractFunction( named: "private var cachedTags: [CachedTag]", from: source ) let filteredItemsSection = try extractFunction( named: "private var filteredItems: [Either]", from: source ) let cacheBuilderCount = tokenCount( matching: #"\bitems\s*\.\s*map\s*\{\s*item\s+in"#, in: cachedTagsSection ) let cachedTagReuseCount = tokenCount( matching: #"\bfor\s+tag\s+in\s+cachedTags\b"#, in: filteredItemsSection ) let stateCacheCount = tokenCount( matching: #"\@State\s+private\s+var\s+cachedTags"#, in: normalizedSource ) let appearRefreshCount = tokenCount( matching: #"\.onAppear\s*\{"#, in: normalizedSource ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( cacheBuilderCount, 0, "Search suggestions should derive cached tags directly from current items." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( cachedTagReuseCount, 0, "Search suggestions should reuse preprocessed cached tags when filtering popup content." ) // swiftlint:disable:next prefer_nimble XCTAssertEqual( stateCacheCount, 0, "Search suggestions should not depend on local state to derive popup content." ) // swiftlint:disable:next prefer_nimble XCTAssertEqual( appearRefreshCount, 0, "Search suggestions should not depend on view-appearance hooks to populate popup content." ) } func testSearchSuggestionsViewShowsFetchedTagsWithoutClientSideRefiltering() throws { let source = try loadSource(at: "Sora/Views/SearchSuggestionsView.swift") let normalizedSource = strippingCommentsAndStrings(from: source) let fetchedTagDirectAppendCount = tokenCount( matching: #"case\s+\.left:\s*guard\s+let\s+name\s*=\s*tag\.names\.first,"# + #"\s*seenTags\.insert\(name\)\.inserted\s*else\s*\{\s*continue\s*\}"# + #"\s*matchingTags\.append\(tag\.original\)"#, in: normalizedSource ) let historyContainsFilterCount = tokenCount( matching: #"case\s+\.right:\s*for\s+name\s+in\s+tag\.names\s*\{"# + #"\s*if\s+name\.contains\(matchCandidateTag\),"# + #"\s*seenTags\.insert\(name\)\.inserted"#, in: normalizedSource ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( fetchedTagDirectAppendCount, 0, "Fetched provider tag suggestions should be displayed directly once returned." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( historyContainsFilterCount, 0, "Search history suggestions should continue filtering against the active tag fragment locally." ) } 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 testSettingsManagerPersistsBooruUserAgentSettings() throws { let source = try loadSource(at: "Sora/Data/Settings/SettingsManager.swift") let sendUserAgentSettingCount = tokenCount( matching: #"\@AppStorage\("sendBooruUserAgent"\)\s*var\s+sendBooruUserAgent\s*=\s*true"#, in: source ) let customUserAgentSettingCount = tokenCount( matching: #"\@AppStorage\("customBooruUserAgent"\)\s*var\s+customBooruUserAgent\s*=\s*"""#, in: source ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( sendUserAgentSettingCount, 0, "SettingsManager should persist a booru User-Agent toggle with a default of true." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( customUserAgentSettingCount, 0, "SettingsManager should persist a custom booru User-Agent override." ) } 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 testMainViewPassesBooruUserAgentSettingsToBooruManager() throws { let source = try loadSource(at: "Sora/Views/MainView.swift") let sendUserAgentWiringCount = tokenCount( matching: #"sendUserAgent:\s*settings\.sendBooruUserAgent"#, in: source ) let customUserAgentWiringCount = tokenCount( matching: #"customUserAgent:\s*settings\.customBooruUserAgent"#, in: source ) let sendUserAgentChangeObserverCount = tokenCount( matching: #"\.onChange\s*\(\s*of:\s*settings\.sendBooruUserAgent\s*\)"#, in: source ) let customUserAgentChangeObserverCount = tokenCount( matching: #"\.onChange\s*\(\s*of:\s*settings\.customBooruUserAgent\s*\)"#, in: source ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( sendUserAgentWiringCount, 1, "MainView should wire booru User-Agent toggle into all BooruManager reconstructions." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( customUserAgentWiringCount, 1, "MainView should wire booru custom User-Agent into all BooruManager reconstructions." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( sendUserAgentChangeObserverCount, 0, "MainView should rebuild BooruManager when booru User-Agent sending changes." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( customUserAgentChangeObserverCount, 0, "MainView should rebuild BooruManager when custom booru User-Agent 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 testBooruManagerMoebooruTagSuggestionsUseNameQuery() throws { let source = try loadSource(at: "Sora/Data/Booru/BooruManager.swift") let tagSearchSection = try extractFunction( named: "private func url(forTagsSearch name: String) -> URL?", from: source ) let moebooruNameQueryCount = tokenCount( matching: #"case\s+\.moebooru:\s*[\s\S]*?URLQueryItem\(name:\s*"name",\s*value:\s*name\)"#, in: tagSearchSection ) let functionNamePatternQueryCount = tokenCount( matching: #"URLQueryItem\(name:\s*"name_pattern""#, in: tagSearchSection ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( moebooruNameQueryCount, 0, "Moebooru tag suggestions should query with `name` so supported providers return filtered matches." ) // swiftlint:disable:next prefer_nimble XCTAssertEqual( functionNamePatternQueryCount, 1, "Only the Gelbooru tag-suggestion branch should still use `name_pattern`." ) } func testBooruManagerDanbooruTagSuggestionsUseJSONSearchEndpoint() throws { let source = try loadSource(at: "Sora/Data/Booru/BooruManager.swift") let searchTagsSection = try extractFunction( named: "func searchTags(name: String) async -> [BooruTag]", from: source ) let tagSearchSection = try extractFunction( named: "private func url(forTagsSearch name: String) -> URL?", from: source ) let danbooruJSONPathCount = tokenCount( matching: #"case\s+\.danbooru:\s*[\s\S]*?components\.path\s*=\s*"/tags\.json""#, in: tagSearchSection ) let danbooruWildcardQueryCount = tokenCount( matching: #"URLQueryItem\(name:\s*"search\[name_matches\]""#, in: tagSearchSection ) let danbooruParserSelectionCount = tokenCount( matching: #"if\s+flavor\s*==\s*\.danbooru\s*\{\s*DanbooruTagParser\(data:\s*data\)\.parse\(\)"#, in: searchTagsSection ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( danbooruJSONPathCount, 0, "Danbooru tag suggestions should use the JSON `/tags.json` endpoint." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( danbooruWildcardQueryCount, 0, "Danbooru tag suggestions should search with `search[name_matches]=*`." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( danbooruParserSelectionCount, 0, "Danbooru tag suggestions should decode JSON through DanbooruTagParser." ) } func testBooruManagerGelbooruTagSuggestionsUseAutocompleteEndpoint() throws { let source = try loadSource(at: "Sora/Data/Booru/BooruManager.swift") let searchTagsSection = try extractFunction( named: "func searchTags(name: String) async -> [BooruTag]", from: source ) let tagSearchSection = try extractFunction( named: "private func url(forTagsSearch name: String) -> URL?", from: source ) let gelbooruAutocompletePageCount = tokenCount( matching: #"URLQueryItem\(name:\s*"page",\s*value:\s*"autocomplete2"\)"#, in: tagSearchSection ) let gelbooruAutocompleteTypeCount = tokenCount( matching: #"URLQueryItem\(name:\s*"type",\s*value:\s*"tag_query"\)"#, in: tagSearchSection ) let safebooruFallbackCount = tokenCount( matching: #"if\s+provider\s*==\s*\.safebooru"#, in: tagSearchSection ) let gelbooruParserSelectionCount = tokenCount( matching: #"flavor\s*==\s*\.gelbooru,\s*provider\s*!=\s*\.safebooru\s*\{\s*"# + #"GelbooruAutocompleteTagParser\(data:\s*data\)\.parse\(\)"#, in: searchTagsSection ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( gelbooruAutocompletePageCount, 0, "Gelbooru tag suggestions should use the public `autocomplete2` endpoint." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( gelbooruAutocompleteTypeCount, 0, "Gelbooru autocomplete requests should identify tag-query lookups explicitly." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( safebooruFallbackCount, 0, "Safebooru should stay on the XML tag API because its autocomplete endpoint redirects away." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( gelbooruParserSelectionCount, 0, "Gelbooru autocomplete responses should decode through GelbooruAutocompleteTagParser." ) } func testBooruRequestConfigurationSupportsOptionalAndCustomUserAgentHeaders() throws { let source = try loadSource(at: "Sora/Data/Booru/BooruRequestConfiguration.swift") let userAgentResolverSection = try extractFunction( named: "static func resolvedUserAgent(", from: source ) let refererResolverSection = try extractFunction( named: "static func baseReferer(for domain: String) -> String", from: source ) let requestBuilderSection = try extractFunction( named: "static func request(", from: source ) let disableBypassCount = tokenCount( matching: #"guard\s+sendUserAgent\s+else\s*\{\s*return\s+nil\s*\}"#, in: userAgentResolverSection ) let customUserAgentTrimCount = tokenCount( matching: #"customUserAgent\.trimmingCharacters"#, in: userAgentResolverSection ) let refererFormatCount = tokenCount( matching: #""https://\\\#\(domain\)/""#, in: refererResolverSection ) let refererHeaderCount = tokenCount( matching: #"request\.setValue\(referer,\s*forHTTPHeaderField:\s*"Referer"\)"#, in: requestBuilderSection ) let explicitUserAgentHeaderCount = tokenCount( matching: #"request\.setValue\(userAgent,\s*forHTTPHeaderField:\s*"User-Agent"\)"#, in: requestBuilderSection ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( disableBypassCount, 0, "Booru request configuration should allow booru requests to omit the User-Agent header." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( customUserAgentTrimCount, 0, "Booru request configuration should normalize custom booru User-Agent values before use." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( refererFormatCount, 0, "Booru request configuration should derive Referer headers from the provider base URL" + " with a trailing slash." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( refererHeaderCount, 0, "Booru request configuration should always attach a Referer header for booru requests." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( explicitUserAgentHeaderCount, 0, "Booru request configuration should continue attaching the User-Agent header only when available." ) } func testBooruManagerUsesSharedRequestConfigurationForAPIRequests() 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 requestBuilderUsageCount = tokenCount( matching: #"BooruRequestConfiguration\.request\("#, in: requestURLSection ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( requestBuilderUsageCount, 0, "BooruManager should build API requests through shared booru request configuration." ) } func testImageCacheManagerUsesHeaderAwareImageRequests() throws { let source = try loadSource(at: "Sora/Data/ImageCacheManager.swift") let preloadSection = try extractFunction( named: "func preloadImages(", from: source ) let loadSection = try extractFunction( named: "func loadImageData(", from: source ) let requestBuilderUsageCount = tokenCount( matching: #"BooruRequestConfiguration\.request\("#, in: preloadSection + loadSection ) let dataTaskRequestCount = tokenCount( matching: #"URLSession\.shared\.dataTask\(with:\s*request\)"#, in: preloadSection ) let dataForRequestCount = tokenCount( matching: #"URLSession\.shared\.data\(for:\s*request\)"#, in: loadSection ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( requestBuilderUsageCount, 1, "ImageCacheManager should build image requests through shared booru request configuration." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( dataTaskRequestCount, 0, "Image preloading should send a header-aware URLRequest." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( dataForRequestCount, 0, "Image loading should send a header-aware URLRequest." ) } func testNetworkImageViewsUseBooruNetworkImageLoader() throws { let postGridSource = try loadSource(at: "Sora/Views/Post/Grid/PostGridThumbnailView.swift") let detailsSource = try loadSource(at: "Sora/Views/Post/Details/PostDetailsImageView.swift") let favoritesSource = try loadSource(at: "Sora/Views/FavoritePostThumbnailView.swift") let loaderUsageCount = tokenCount( matching: #"\.networkImageLoader\(networkImageLoader\)"#, in: postGridSource + detailsSource + favoritesSource ) let loaderConstructionCount = tokenCount( matching: #"BooruNetworkImageLoader\("#, in: postGridSource + detailsSource + favoritesSource ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( loaderUsageCount, 2, "Image views should inject a booru-aware network image loader so CDN requests include required headers." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( loaderConstructionCount, 2, "Image views should construct booru-aware loaders using the current provider domain and request settings." ) } 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) } }