From ef0997a1775494bcc8008d0a752e2f790869eca7 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Mon, 10 Feb 2025 12:51:03 +0100 Subject: [PATCH 1/8] Add initial tests for History Data Client --- DuckDuckGo-macOS.xcodeproj/project.pbxproj | 18 +++++ DuckDuckGo/Application/AppDelegate.swift | 2 +- .../Services/HistoryViewCoordinator.swift | 44 +++++++++++ .../Services/HistoryViewErrorHandler.swift | 38 ++++++++++ .../HistoryViewActionsManagerExtension.swift | 7 +- DuckDuckGo/Statistics/HistoryViewPixel.swift | 76 +++++++++++++++++++ .../Tab/View/BrowserTabViewController.swift | 2 +- .../Sources/HistoryView/DataClient.swift | 17 +++-- .../Sources/HistoryView/DataModel.swift | 4 + .../HistoryViewTests/DataClientTests.swift | 66 ++++++++++++++++ .../Helpers/MessageHelper.swift | 57 ++++++++++++++ .../CapturingActionsHandler.swift} | 14 ++-- .../Mocks/CapturingDataProvider.swift | 49 ++++++++++++ .../Mocks/CapturingErrorHandler.swift | 43 +++++++++++ 14 files changed, 422 insertions(+), 15 deletions(-) create mode 100644 DuckDuckGo/History/Services/HistoryViewCoordinator.swift create mode 100644 DuckDuckGo/History/Services/HistoryViewErrorHandler.swift create mode 100644 DuckDuckGo/Statistics/HistoryViewPixel.swift create mode 100644 LocalPackages/HistoryView/Tests/HistoryViewTests/DataClientTests.swift create mode 100644 LocalPackages/HistoryView/Tests/HistoryViewTests/Helpers/MessageHelper.swift rename LocalPackages/HistoryView/Tests/HistoryViewTests/{HistoryViewTests.swift => Mocks/CapturingActionsHandler.swift} (74%) create mode 100644 LocalPackages/HistoryView/Tests/HistoryViewTests/Mocks/CapturingDataProvider.swift create mode 100644 LocalPackages/HistoryView/Tests/HistoryViewTests/Mocks/CapturingErrorHandler.swift diff --git a/DuckDuckGo-macOS.xcodeproj/project.pbxproj b/DuckDuckGo-macOS.xcodeproj/project.pbxproj index acd18629c5..b4edb6f41f 100644 --- a/DuckDuckGo-macOS.xcodeproj/project.pbxproj +++ b/DuckDuckGo-macOS.xcodeproj/project.pbxproj @@ -1182,6 +1182,12 @@ 37534CA8281198CD002621E7 /* AdjacentItemEnumerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37534CA62811988E002621E7 /* AdjacentItemEnumerator.swift */; }; 3758A38C2D108233001CEAA1 /* NewTabPageFreemiumDBPBannerProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3758A38B2D108229001CEAA1 /* NewTabPageFreemiumDBPBannerProvider.swift */; }; 3758A38D2D108233001CEAA1 /* NewTabPageFreemiumDBPBannerProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3758A38B2D108229001CEAA1 /* NewTabPageFreemiumDBPBannerProvider.swift */; }; + 37598EF12D5A187900720EAF /* HistoryViewErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37598EF02D5A187600720EAF /* HistoryViewErrorHandler.swift */; }; + 37598EF22D5A187900720EAF /* HistoryViewErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37598EF02D5A187600720EAF /* HistoryViewErrorHandler.swift */; }; + 37598EF42D5A18FF00720EAF /* HistoryViewPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37598EF32D5A18FB00720EAF /* HistoryViewPixel.swift */; }; + 37598EF52D5A18FF00720EAF /* HistoryViewPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37598EF32D5A18FB00720EAF /* HistoryViewPixel.swift */; }; + 37598EF72D5A1D5800720EAF /* HistoryViewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37598EF62D5A1D5500720EAF /* HistoryViewCoordinator.swift */; }; + 37598EF82D5A1D5800720EAF /* HistoryViewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37598EF62D5A1D5500720EAF /* HistoryViewCoordinator.swift */; }; 376113CC2B29CD5B00E794BB /* CriticalPathsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 565E46DF2B2725DD0013AC2A /* CriticalPathsTests.swift */; }; 376705AF27EB488600DD8D76 /* RoundedSelectionRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0511B3262CAA5A00F6079C /* RoundedSelectionRowView.swift */; }; 376731822C7E226A00EB097B /* HomePageViewBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376731812C7E226A00EB097B /* HomePageViewBackground.swift */; }; @@ -3813,6 +3819,9 @@ 37534CA42811987D002621E7 /* AdjacentItemEnumeratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdjacentItemEnumeratorTests.swift; sourceTree = ""; }; 37534CA62811988E002621E7 /* AdjacentItemEnumerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdjacentItemEnumerator.swift; sourceTree = ""; }; 3758A38B2D108229001CEAA1 /* NewTabPageFreemiumDBPBannerProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageFreemiumDBPBannerProvider.swift; sourceTree = ""; }; + 37598EF02D5A187600720EAF /* HistoryViewErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryViewErrorHandler.swift; sourceTree = ""; }; + 37598EF32D5A18FB00720EAF /* HistoryViewPixel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryViewPixel.swift; sourceTree = ""; }; + 37598EF62D5A1D5500720EAF /* HistoryViewCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryViewCoordinator.swift; sourceTree = ""; }; 376113C52B29BCD600E794BB /* SyncE2EUITests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = SyncE2EUITests.xcconfig; sourceTree = ""; }; 376113D42B29CD5B00E794BB /* SyncE2EUITests App Store.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "SyncE2EUITests App Store.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 376113D72B29D0F800E794BB /* SyncE2EUITestsAppStore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = SyncE2EUITestsAppStore.xcconfig; sourceTree = ""; }; @@ -9235,8 +9244,10 @@ AAE75276263B038A00B973F8 /* Services */ = { isa = PBXGroup; children = ( + 37598EF62D5A1D5500720EAF /* HistoryViewCoordinator.swift */, 37C749392D55FE690065B48B /* HistoryViewActionsHandler.swift */, 37E13B8D2D54B023002ECD62 /* HistoryViewDataProvider.swift */, + 37598EF02D5A187600720EAF /* HistoryViewErrorHandler.swift */, 3745DE0A2D53969000024FC8 /* HistoryDebugMenu.swift */, AAE75278263B046100B973F8 /* History.xcdatamodeld */, AAE7527B263B056C00B973F8 /* EncryptedHistoryStore.swift */, @@ -9633,6 +9644,7 @@ F188267F2BBEB58100D9AC4F /* PrivacyProPixel.swift */, 370270BB2C78AC6F002E44E4 /* NewTabBackgroundPixel.swift */, 37E307AD2D07482F00599500 /* NewTabPagePixel.swift */, + 37598EF32D5A18FB00720EAF /* HistoryViewPixel.swift */, 56DB9FE82CD24B47001BEC23 /* ContextualOnboardingPixel.swift */, 37219B362CBFBC8200C9D7A8 /* NewTabSearchBoxExperimentPixel.swift */, ); @@ -11716,6 +11728,7 @@ EED4D3D92C874AE200C79EEA /* PopoverInfoViewController.swift in Sources */, 3707C722294B5D2900682A9F /* WKWebViewExtension.swift in Sources */, 3745DE052D536DCF00024FC8 /* HistoryGroupingProvider.swift in Sources */, + 37598EF52D5A18FF00720EAF /* HistoryViewPixel.swift in Sources */, 3706FAD9293F65D500E42796 /* FirefoxFaviconsReader.swift in Sources */, 3706FADB293F65D500E42796 /* ContentBlockingRulesUpdateObserver.swift in Sources */, 3706FADC293F65D500E42796 /* FirefoxLoginReader.swift in Sources */, @@ -11971,6 +11984,7 @@ 84DDB90B2C92B66E008C997B /* WKVisitedLinkStoreWrapper.swift in Sources */, F1C70D7A2BFF50A400599292 /* DataBrokerProtectionLoginItemInterface.swift in Sources */, 377D801F2AB48191002AF251 /* FavoritesDisplayModeSyncHandler.swift in Sources */, + 37598EF12D5A187900720EAF /* HistoryViewErrorHandler.swift in Sources */, 3706FB6F293F65D500E42796 /* BookmarkListViewController.swift in Sources */, 3706FB72293F65D500E42796 /* RecentlyClosedCoordinator.swift in Sources */, 3706FB74293F65D500E42796 /* FaviconHostReference.swift in Sources */, @@ -12090,6 +12104,7 @@ 1DDD3EC12B84F5D5004CBF2B /* PreferencesCookiePopupProtectionView.swift in Sources */, 3706FBA4293F65D500E42796 /* ContentOverlayPopover.swift in Sources */, 3706FBA5293F65D500E42796 /* TabShadowView.swift in Sources */, + 37598EF82D5A1D5800720EAF /* HistoryViewCoordinator.swift in Sources */, 3706FBA7293F65D500E42796 /* EncryptedValueTransformer.swift in Sources */, 4B41EDAF2B168AFF001EEDF4 /* UnifiedFeedbackFormViewController.swift in Sources */, 31EF1E802B63FFA800E6DB17 /* DBPHomeViewController.swift in Sources */, @@ -13697,6 +13712,7 @@ 857E5AF52A79045800FC0FB4 /* PixelExperiment.swift in Sources */, B6C416A7294A4AE500C4F2E7 /* DuckPlayerTabExtension.swift in Sources */, BDBA859F2C5D25B700BC54F5 /* VPNMetadataCollector.swift in Sources */, + 37598EF72D5A1D5800720EAF /* HistoryViewCoordinator.swift in Sources */, AA5C1DD5285C780C0089850C /* RecentlyClosedCoordinator.swift in Sources */, 376E8C1D2D41920A00D5D2EC /* RecentActivityProvider.swift in Sources */, AA88D14B252A557100980B4E /* URLRequestExtension.swift in Sources */, @@ -14121,6 +14137,7 @@ 9F56CFAD2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift in Sources */, 1D4071AE2BD64267002D4537 /* DockCustomizer.swift in Sources */, AA585D82248FD31100E9A3E2 /* AppDelegate.swift in Sources */, + 37598EF42D5A18FF00720EAF /* HistoryViewPixel.swift in Sources */, 7B1E81A027C8874900FF0E60 /* ContentOverlayViewController.swift in Sources */, CD2AB5C12C8222F40019EB49 /* MaliciousSiteProtectionPreferences.swift in Sources */, 372D15EC2D00FA1A00A11576 /* AppearancePreferences+NewTabPage.swift in Sources */, @@ -14177,6 +14194,7 @@ B6F9BDE42B45CD1900677B33 /* ModalView.swift in Sources */, 376731A12C7F50D200EB097B /* Logger+HomePageSettings.swift in Sources */, F18826842BBEE31700D9AC4F /* PixelKit+Assertion.swift in Sources */, + 37598EF22D5A187900720EAF /* HistoryViewErrorHandler.swift in Sources */, 1D2DC0072901679C008083A1 /* BWError.swift in Sources */, 853014D625E671A000FB8205 /* PageObserverUserScript.swift in Sources */, B677FC4F2B06376B0099EB04 /* ReportFeedbackView.swift in Sources */, diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index 5945932e4b..6b644e7231 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -97,7 +97,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { let bookmarksManager = LocalBookmarkManager.shared var privacyDashboardWindow: NSWindow? - private(set) lazy var historyViewActionsManager: HistoryViewActionsManager = HistoryViewActionsManager() + private(set) lazy var historyViewCoordinator: HistoryViewCoordinator = HistoryViewCoordinator(historyCoordinator: HistoryCoordinator.shared) private(set) lazy var newTabPageCoordinator: NewTabPageCoordinator = NewTabPageCoordinator( appearancePreferences: .shared, settingsModel: homePageSettingsModel, diff --git a/DuckDuckGo/History/Services/HistoryViewCoordinator.swift b/DuckDuckGo/History/Services/HistoryViewCoordinator.swift new file mode 100644 index 0000000000..a317c0d2b9 --- /dev/null +++ b/DuckDuckGo/History/Services/HistoryViewCoordinator.swift @@ -0,0 +1,44 @@ +// +// HistoryViewCoordinator.swift +// +// Copyright © 2025 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import Foundation +import HistoryView +import Persistence +import PixelKit + +final class HistoryViewCoordinator { + let actionsManager: HistoryViewActionsManager + + init( + historyCoordinator: HistoryGroupingDataSource, + notificationCenter: NotificationCenter = .default, + fireDailyPixel: @escaping (PixelKitEvent) -> Void = { PixelKit.fire($0, frequency: .daily) } + ) { + actionsManager = HistoryViewActionsManager(historyCoordinator: historyCoordinator) + + notificationCenter.publisher(for: .historyWebViewDidAppear) + .prefix(1) + .sink { _ in + fireDailyPixel(HistoryViewPixel.historyPageShown) + } + .store(in: &cancellables) + } + + private var cancellables: Set = [] +} diff --git a/DuckDuckGo/History/Services/HistoryViewErrorHandler.swift b/DuckDuckGo/History/Services/HistoryViewErrorHandler.swift new file mode 100644 index 0000000000..3b16b13050 --- /dev/null +++ b/DuckDuckGo/History/Services/HistoryViewErrorHandler.swift @@ -0,0 +1,38 @@ +// +// HistoryViewErrorHandler.swift +// +// Copyright © 2025 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Common +import HistoryView +import PixelKit + +final class HistoryViewErrorHandler: EventMapping { + + init() { + super.init { event, _, _, _ in + switch event { + case .historyViewError(let message): + PixelKit.fire(DebugEvent(HistoryViewPixel.historyPageExceptionReported(message: message)), frequency: .dailyAndStandard) + } + } + } + + override init(mapping: @escaping EventMapping.Mapping) { + fatalError("Use init()") + } +} + diff --git a/DuckDuckGo/History/View/HistoryViewActionsManagerExtension.swift b/DuckDuckGo/History/View/HistoryViewActionsManagerExtension.swift index f3050a087f..6616b69e2c 100644 --- a/DuckDuckGo/History/View/HistoryViewActionsManagerExtension.swift +++ b/DuckDuckGo/History/View/HistoryViewActionsManagerExtension.swift @@ -21,11 +21,12 @@ import HistoryView extension HistoryViewActionsManager { - convenience init() { + convenience init(historyCoordinator: HistoryGroupingDataSource) { self.init(scriptClients: [ DataClient( - dataProvider: HistoryViewDataProvider(historyGroupingDataSource: HistoryCoordinator.shared), - actionsHandler: HistoryViewActionsHandler() + dataProvider: HistoryViewDataProvider(historyGroupingDataSource: historyCoordinator), + actionsHandler: HistoryViewActionsHandler(), + errorHandler: HistoryViewErrorHandler() ) ]) } diff --git a/DuckDuckGo/Statistics/HistoryViewPixel.swift b/DuckDuckGo/Statistics/HistoryViewPixel.swift new file mode 100644 index 0000000000..107fba7873 --- /dev/null +++ b/DuckDuckGo/Statistics/HistoryViewPixel.swift @@ -0,0 +1,76 @@ +// +// HistoryViewPixel.swift +// +// Copyright © 2025 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import PixelKit + +/** + * This enum keeps pixels related to HTML History View. + */ +enum HistoryViewPixel: PixelKitEventV2 { + + /** + * Event Trigger: History View is displayed to user. + * + * > Note: This is a daily pixel. + * + * > Related links: + * [Privacy Triage TBD]() + * [Detailed Pixels description](https://app.asana.com/0/0/1209364382402737/f) + * + * Anomaly Investigation: + * - Anomaly in this pixel may mean an increase/drop in app use. + */ + case historyPageShown + + // MARK: - Debug + + /** + * Event Trigger: History View reports a JavaScript exception. + * + * > Note: This is a daily + standard pixel. + * + * > Related links: + * [Privacy Triage TBD]() + * [Detailed Pixels description](https://app.asana.com/0/0/1209364382402737/f) + * + * Anomaly Investigation: + * - Anomaly in this pixel may mean a critical breakage in the History View. + */ + case historyPageExceptionReported(message: String) + + var name: String { + switch self { + case .historyPageShown: return "history-page_shown" + case .historyPageExceptionReported: return "history-page_exception-reported" + } + } + + var parameters: [String: String]? { + switch self { + case .historyPageShown: + return nil + case .historyPageExceptionReported(let message): + return [PixelKit.Parameters.assertionMessage: message] + } + } + + var error: (any Error)? { + nil + } +} diff --git a/DuckDuckGo/Tab/View/BrowserTabViewController.swift b/DuckDuckGo/Tab/View/BrowserTabViewController.swift index 6b0ceb523b..6413acd82a 100644 --- a/DuckDuckGo/Tab/View/BrowserTabViewController.swift +++ b/DuckDuckGo/Tab/View/BrowserTabViewController.swift @@ -102,7 +102,7 @@ final class BrowserTabViewController: NSViewController { onboardingDialogFactory: ContextualDaxDialogsFactory = DefaultContextualDaxDialogViewFactory(), featureFlagger: FeatureFlagger = NSApp.delegateTyped.featureFlagger, newTabPageActionsManager: NewTabPageActionsManager = NSApp.delegateTyped.newTabPageCoordinator.actionsManager, - historyViewActionsManager: HistoryViewActionsManager = NSApp.delegateTyped.historyViewActionsManager, + historyViewActionsManager: HistoryViewActionsManager = NSApp.delegateTyped.historyViewCoordinator.actionsManager, activeRemoteMessageModel: ActiveRemoteMessageModel = NSApp.delegateTyped.activeRemoteMessageModel ) { self.tabCollectionViewModel = tabCollectionViewModel diff --git a/LocalPackages/HistoryView/Sources/HistoryView/DataClient.swift b/LocalPackages/HistoryView/Sources/HistoryView/DataClient.swift index 4cb5ce03dd..64529f2813 100644 --- a/LocalPackages/HistoryView/Sources/HistoryView/DataClient.swift +++ b/LocalPackages/HistoryView/Sources/HistoryView/DataClient.swift @@ -42,15 +42,21 @@ public protocol DataProviding: AnyObject { func visits(for query: DataModel.HistoryQueryKind, limit: Int, offset: Int) async -> DataModel.HistoryItemsBatch } +public enum HistoryViewEvent: Equatable { + case historyViewError(message: String) +} + public final class DataClient: HistoryViewUserScriptClient { private var cancellables = Set() private let dataProvider: DataProviding private let actionsHandler: ActionsHandling + private let errorHandler: EventMapping? - public init(dataProvider: DataProviding, actionsHandler: ActionsHandling) { + public init(dataProvider: DataProviding, actionsHandler: ActionsHandling, errorHandler: EventMapping?) { self.dataProvider = dataProvider self.actionsHandler = actionsHandler + self.errorHandler = errorHandler super.init() } @@ -115,10 +121,11 @@ public final class DataClient: HistoryViewUserScriptClient { } private func reportException(params: Any, original: WKScriptMessage) async throws -> Encodable? { - guard let params = params as? [String: String] else { return nil } - let message = params["message"] ?? "" - let id = params["id"] ?? "" - Logger.general.error("New Tab Page error: \("\(id): \(message)", privacy: .public)") + guard let exception: DataModel.Exception = DecodableHelper.decode(from: params) else { + return nil + } + errorHandler?.fire(.historyViewError(message: exception.message)) + Logger.general.error("History View error: \("\(exception.message)", privacy: .public)") return nil } } diff --git a/LocalPackages/HistoryView/Sources/HistoryView/DataModel.swift b/LocalPackages/HistoryView/Sources/HistoryView/DataModel.swift index 3e95f80905..4301e73ddb 100644 --- a/LocalPackages/HistoryView/Sources/HistoryView/DataModel.swift +++ b/LocalPackages/HistoryView/Sources/HistoryView/DataModel.swift @@ -129,6 +129,10 @@ extension DataModel { } } + struct Exception: Codable, Equatable { + let message: String + } + struct GetRangesResponse: Codable, Equatable { let ranges: [HistoryRange] } diff --git a/LocalPackages/HistoryView/Tests/HistoryViewTests/DataClientTests.swift b/LocalPackages/HistoryView/Tests/HistoryViewTests/DataClientTests.swift new file mode 100644 index 0000000000..4936e520e9 --- /dev/null +++ b/LocalPackages/HistoryView/Tests/HistoryViewTests/DataClientTests.swift @@ -0,0 +1,66 @@ +// +// DataClientTests.swift +// +// Copyright © 2025 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +@testable import HistoryView + +final class DataClientTests: XCTestCase { + private var client: DataClient! + private var dataProvider: CapturingDataProvider! + private var actionsHandler: CapturingActionsHandler! + private var errorHandler: CapturingErrorHandler! + private var userScript: HistoryViewUserScript! + private var messageHelper: MessageHelper! + + override func setUpWithError() throws { + try super.setUpWithError() + dataProvider = CapturingDataProvider() + actionsHandler = CapturingActionsHandler() + errorHandler = CapturingErrorHandler() + client = DataClient(dataProvider: dataProvider, actionsHandler: actionsHandler, errorHandler: errorHandler) + + userScript = HistoryViewUserScript() + messageHelper = .init(userScript: userScript) + client.registerMessageHandlers(for: userScript) + } + + // MARK: - initialSetup + + func testThatInitialSetupReturnsConfiguration() async throws { + let configuration: DataModel.Configuration = try await messageHelper.handleMessage(named: .initialSetup) + XCTAssertEqual(configuration.platform, .init(name: "macos")) + } + + // MARK: - reportInitException + + func testThatReportInitExceptionForwardsEventToTheMapper() async throws { + let exception = DataModel.Exception(message: "sample message") + try await messageHelper.handleMessageExpectingNilResponse(named: .reportInitException, parameters: exception) + + XCTAssertEqual(errorHandler.events, [.historyViewError(message: "sample message")]) + } + + // MARK: - reportPageException + + func testThatReportPageExceptionForwardsEventToTheMapper() async throws { + let exception = DataModel.Exception(message: "sample message") + try await messageHelper.handleMessageExpectingNilResponse(named: .reportPageException, parameters: exception) + + XCTAssertEqual(errorHandler.events, [.historyViewError(message: "sample message")]) + } +} diff --git a/LocalPackages/HistoryView/Tests/HistoryViewTests/Helpers/MessageHelper.swift b/LocalPackages/HistoryView/Tests/HistoryViewTests/Helpers/MessageHelper.swift new file mode 100644 index 0000000000..ceaa6b51e4 --- /dev/null +++ b/LocalPackages/HistoryView/Tests/HistoryViewTests/Helpers/MessageHelper.swift @@ -0,0 +1,57 @@ +// +// MessageHelper.swift +// +// Copyright © 2025 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import HistoryView +import XCTest + +final class MessageHelper where MessageName.RawValue == String { + let userScript: HistoryViewUserScript + + init(userScript: HistoryViewUserScript) { + self.userScript = userScript + } + + func handleMessage(named methodName: MessageName, parameters: Any = [], file: StaticString = #file, line: UInt = #line) async throws -> Response { + let handler = try XCTUnwrap(userScript.handler(forMethodNamed: methodName.rawValue), file: file, line: line) + let response = try await handler(Self.asJSON(parameters), .init()) + return try XCTUnwrap(response as? Response, file: file, line: line) + } + + func handleMessageIgnoringResponse(named methodName: MessageName, parameters: Any = [], file: StaticString = #file, line: UInt = #line) async throws { + let handler = try XCTUnwrap(userScript.handler(forMethodNamed: methodName.rawValue), file: file, line: line) + _ = try await handler(Self.asJSON(parameters), .init()) + } + + func handleMessageExpectingNilResponse(named methodName: MessageName, parameters: Any = [], file: StaticString = #file, line: UInt = #line) async throws { + let handler = try XCTUnwrap(userScript.handler(forMethodNamed: methodName.rawValue), file: file, line: line) + let response = try await handler(Self.asJSON(parameters), .init()) + XCTAssertNil(response, file: file, line: line) + } + + private static func asJSON(_ value: Any, file: StaticString = #file, line: UInt = #line) throws -> Any { + if JSONSerialization.isValidJSONObject(value) { + return value + } + if let encodableValue = value as? Encodable { + let jsonData = try JSONEncoder().encode(encodableValue) + return try JSONSerialization.jsonObject(with: jsonData) + } + XCTFail("invalid JSON value", file: file, line: line) + return [] + } +} diff --git a/LocalPackages/HistoryView/Tests/HistoryViewTests/HistoryViewTests.swift b/LocalPackages/HistoryView/Tests/HistoryViewTests/Mocks/CapturingActionsHandler.swift similarity index 74% rename from LocalPackages/HistoryView/Tests/HistoryViewTests/HistoryViewTests.swift rename to LocalPackages/HistoryView/Tests/HistoryViewTests/Mocks/CapturingActionsHandler.swift index d6ab6cba2c..cf91da5d99 100644 --- a/LocalPackages/HistoryView/Tests/HistoryViewTests/HistoryViewTests.swift +++ b/LocalPackages/HistoryView/Tests/HistoryViewTests/Mocks/CapturingActionsHandler.swift @@ -1,5 +1,5 @@ // -// HistoryViewTests.swift +// CapturingActionsHandler.swift // // Copyright © 2025 DuckDuckGo. All rights reserved. // @@ -16,9 +16,13 @@ // limitations under the License. // -import Testing -@testable import HistoryView +import Foundation +import HistoryView -@Test func example() async throws { - // Write your test here and use APIs like `#expect(...)` to check expected conditions. +final class CapturingActionsHandler: ActionsHandling { + func open(_ url: URL) { + openCalls.append(url) + } + + var openCalls: [URL] = [] } diff --git a/LocalPackages/HistoryView/Tests/HistoryViewTests/Mocks/CapturingDataProvider.swift b/LocalPackages/HistoryView/Tests/HistoryViewTests/Mocks/CapturingDataProvider.swift new file mode 100644 index 0000000000..e014c0ac85 --- /dev/null +++ b/LocalPackages/HistoryView/Tests/HistoryViewTests/Mocks/CapturingDataProvider.swift @@ -0,0 +1,49 @@ +// +// CapturingDataProvider.swift +// +// Copyright © 2025 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import HistoryView + +final class CapturingDataProvider: DataProviding { + var ranges: [DataModel.HistoryRange] { + rangesCallCount += 1 + return _ranges + } + + func resetCache() { + resetCacheCallCount += 1 + } + + func visits(for query: DataModel.HistoryQueryKind, limit: Int, offset: Int) async -> DataModel.HistoryItemsBatch { + visitsCalls.append(.init(query: query, limit: limit, offset: offset)) + return await visits(query, limit, offset) + } + + // swiftlint:disable:next identifier_name + var _ranges: [DataModel.HistoryRange] = [] + var rangesCallCount: Int = 0 + var resetCacheCallCount: Int = 0 + + var visitsCalls: [VisitsCall] = [] + var visits: (DataModel.HistoryQueryKind, Int, Int) async -> DataModel.HistoryItemsBatch = { _, _, _ in .init(finished: true, visits: []) } + + struct VisitsCall: Equatable { + let query: DataModel.HistoryQueryKind + let limit: Int + let offset: Int + } +} diff --git a/LocalPackages/HistoryView/Tests/HistoryViewTests/Mocks/CapturingErrorHandler.swift b/LocalPackages/HistoryView/Tests/HistoryViewTests/Mocks/CapturingErrorHandler.swift new file mode 100644 index 0000000000..33700dc0d1 --- /dev/null +++ b/LocalPackages/HistoryView/Tests/HistoryViewTests/Mocks/CapturingErrorHandler.swift @@ -0,0 +1,43 @@ +// +// CapturingErrorHandler.swift +// +// Copyright © 2025 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import Common +import HistoryView + +final class CapturingErrorHandler: EventMapping { + var events: [HistoryViewEvent] = [] + + init() { + let localEvents = PassthroughSubject() + super.init { event, _, _, _ in + localEvents.send(event) + } + + cancellable = localEvents + .sink { [weak self] value in + self?.events.append(value) + } + } + + override init(mapping: @escaping EventMapping.Mapping) { + fatalError("Use init()") + } + + private var cancellable: AnyCancellable? +} From 7b51fa71b2dc3e8bc19cdb7386ccef53847ee144 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Mon, 10 Feb 2025 13:21:18 +0100 Subject: [PATCH 2/8] Add remaining tests for Data Client --- .../Sources/HistoryView/DataModel.swift | 6 ++-- .../HistoryViewTests/DataClientTests.swift | 34 +++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/LocalPackages/HistoryView/Sources/HistoryView/DataModel.swift b/LocalPackages/HistoryView/Sources/HistoryView/DataModel.swift index 4301e73ddb..022e496891 100644 --- a/LocalPackages/HistoryView/Sources/HistoryView/DataModel.swift +++ b/LocalPackages/HistoryView/Sources/HistoryView/DataModel.swift @@ -81,14 +81,14 @@ public enum DataModel { } public struct HistoryQuery: Codable, Equatable { + let query: HistoryQueryKind let limit: Int let offset: Int - let query: HistoryQueryKind - public init(limit: Int, offset: Int, query: HistoryQueryKind) { + public init(query: HistoryQueryKind, limit: Int, offset: Int) { + self.query = query self.limit = limit self.offset = offset - self.query = query } } diff --git a/LocalPackages/HistoryView/Tests/HistoryViewTests/DataClientTests.swift b/LocalPackages/HistoryView/Tests/HistoryViewTests/DataClientTests.swift index 4936e520e9..62ea420c59 100644 --- a/LocalPackages/HistoryView/Tests/HistoryViewTests/DataClientTests.swift +++ b/LocalPackages/HistoryView/Tests/HistoryViewTests/DataClientTests.swift @@ -46,6 +46,40 @@ final class DataClientTests: XCTestCase { XCTAssertEqual(configuration.platform, .init(name: "macos")) } + func testThatInitialSetupResetsDataProviderCache() async throws { + try await messageHelper.handleMessageIgnoringResponse(named: .initialSetup) + XCTAssertEqual(dataProvider.resetCacheCallCount, 1) + } + + // MARK: - getRanges + + func testThatGetRangesReturnsRangesFromDataProvider() async throws { + dataProvider._ranges = [.all, .friday, .recentlyOpened] + let rangesResponse: DataModel.GetRangesResponse = try await messageHelper.handleMessage(named: .getRanges) + XCTAssertEqual(dataProvider.rangesCallCount, 1) + XCTAssertEqual(rangesResponse.ranges, [.all, .friday, .recentlyOpened]) + } + + // MARK: - query + + func testThatQueryReturnsDataFromProviderAndEchoesQueryKind() async throws { + let historyItem = DataModel.HistoryItem(id: "1", url: "https://example.com", title: "Example.com", domain: "example.com", etldPlusOne: "example.com", dateRelativeDay: "Today", dateShort: "", dateTimeOfDay: "10:08") + dataProvider.visits = { _, _, _ in return .init(finished: true, visits: [historyItem]) } + let query = DataModel.HistoryQuery(query: .searchTerm(""), limit: 150, offset: 0) + + let queryResponse: DataModel.HistoryQueryResponse = try await messageHelper.handleMessage(named: .query, parameters: query) + XCTAssertEqual(dataProvider.visitsCalls.count, 1) + XCTAssertEqual(queryResponse, .init(info: .init(finished: true, query: query.query), value: [historyItem])) + } + + // MARK: - open + + func testThatOpenCallsActionHandler() async throws { + let url = "https://example.com" + try await messageHelper.handleMessageExpectingNilResponse(named: .open, parameters: DataModel.HistoryOpenAction(url: url)) + XCTAssertEqual(actionsHandler.openCalls, [try XCTUnwrap(URL(string: url))]) + } + // MARK: - reportInitException func testThatReportInitExceptionForwardsEventToTheMapper() async throws { From 271b218783db67ef5132cdb0d210fde0653d8804 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Mon, 10 Feb 2025 13:23:22 +0100 Subject: [PATCH 3/8] ActionsHandler -> ActionsHandling --- .../HistoryView/{ActionsHandler.swift => ActionsHandling.swift} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename LocalPackages/HistoryView/Sources/HistoryView/{ActionsHandler.swift => ActionsHandling.swift} (96%) diff --git a/LocalPackages/HistoryView/Sources/HistoryView/ActionsHandler.swift b/LocalPackages/HistoryView/Sources/HistoryView/ActionsHandling.swift similarity index 96% rename from LocalPackages/HistoryView/Sources/HistoryView/ActionsHandler.swift rename to LocalPackages/HistoryView/Sources/HistoryView/ActionsHandling.swift index 19c24ee436..78b43ec8da 100644 --- a/LocalPackages/HistoryView/Sources/HistoryView/ActionsHandler.swift +++ b/LocalPackages/HistoryView/Sources/HistoryView/ActionsHandling.swift @@ -1,5 +1,5 @@ // -// ActionsHandler.swift +// ActionsHandling.swift // // Copyright © 2025 DuckDuckGo. All rights reserved. // From c986bcebd4a087bb23d469e3038a1a588e0dc59d Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Mon, 10 Feb 2025 13:35:07 +0100 Subject: [PATCH 4/8] Add ArrayExtensionTests --- DuckDuckGo-macOS.xcodeproj/project.pbxproj | 6 ++ .../Common/Extensions/ArrayExtension.swift | 2 +- .../Extensions/ArrayExtensionTests.swift | 58 +++++++++++++++++++ 3 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 UnitTests/Common/Extensions/ArrayExtensionTests.swift diff --git a/DuckDuckGo-macOS.xcodeproj/project.pbxproj b/DuckDuckGo-macOS.xcodeproj/project.pbxproj index b4edb6f41f..dce4ab9d19 100644 --- a/DuckDuckGo-macOS.xcodeproj/project.pbxproj +++ b/DuckDuckGo-macOS.xcodeproj/project.pbxproj @@ -1188,6 +1188,8 @@ 37598EF52D5A18FF00720EAF /* HistoryViewPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37598EF32D5A18FB00720EAF /* HistoryViewPixel.swift */; }; 37598EF72D5A1D5800720EAF /* HistoryViewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37598EF62D5A1D5500720EAF /* HistoryViewCoordinator.swift */; }; 37598EF82D5A1D5800720EAF /* HistoryViewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37598EF62D5A1D5500720EAF /* HistoryViewCoordinator.swift */; }; + 37598EFA2D5A278400720EAF /* ArrayExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37598EF92D5A278100720EAF /* ArrayExtensionTests.swift */; }; + 37598EFB2D5A278400720EAF /* ArrayExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37598EF92D5A278100720EAF /* ArrayExtensionTests.swift */; }; 376113CC2B29CD5B00E794BB /* CriticalPathsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 565E46DF2B2725DD0013AC2A /* CriticalPathsTests.swift */; }; 376705AF27EB488600DD8D76 /* RoundedSelectionRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0511B3262CAA5A00F6079C /* RoundedSelectionRowView.swift */; }; 376731822C7E226A00EB097B /* HomePageViewBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376731812C7E226A00EB097B /* HomePageViewBackground.swift */; }; @@ -3822,6 +3824,7 @@ 37598EF02D5A187600720EAF /* HistoryViewErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryViewErrorHandler.swift; sourceTree = ""; }; 37598EF32D5A18FB00720EAF /* HistoryViewPixel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryViewPixel.swift; sourceTree = ""; }; 37598EF62D5A1D5500720EAF /* HistoryViewCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryViewCoordinator.swift; sourceTree = ""; }; + 37598EF92D5A278100720EAF /* ArrayExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayExtensionTests.swift; sourceTree = ""; }; 376113C52B29BCD600E794BB /* SyncE2EUITests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = SyncE2EUITests.xcconfig; sourceTree = ""; }; 376113D42B29CD5B00E794BB /* SyncE2EUITests App Store.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "SyncE2EUITests App Store.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 376113D72B29D0F800E794BB /* SyncE2EUITestsAppStore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = SyncE2EUITestsAppStore.xcconfig; sourceTree = ""; }; @@ -9298,6 +9301,7 @@ AAEC74B92642E66600C2EFBC /* Extensions */ = { isa = PBXGroup; children = ( + 37598EF92D5A278100720EAF /* ArrayExtensionTests.swift */, 84B479072CCA7A3900F40329 /* Logger+UnitTests.swift */, 4B4F72EB266B2ED300814C60 /* CollectionExtension.swift */, 1DFAB51F2A89830D00A0F7F6 /* SetExtensionTests.swift */, @@ -12878,6 +12882,7 @@ 3707C72F294B5D4F00682A9F /* WebViewTests.swift in Sources */, 021EA0852BD6E0EB00772C9A /* TabsPreferencesTests.swift in Sources */, 5682C69429B79B57004DE3C8 /* TabBarViewItemTests.swift in Sources */, + 37598EFA2D5A278400720EAF /* ArrayExtensionTests.swift in Sources */, 3706FE77293F661700E42796 /* PreferencesSidebarModelTests.swift in Sources */, 5650E3742D3FDC8900D41ECF /* PageRefreshMonitorExtensionTests.swift in Sources */, 3706FE78293F661700E42796 /* HistoryCoordinatingMock.swift in Sources */, @@ -14551,6 +14556,7 @@ 567A23CD2C80CE060010F66C /* SpecialErrorPageUserScriptTests.swift in Sources */, 9F0FFFB82BCCAE9C007C87DD /* AddEditBookmarkDialogViewModelMock.swift in Sources */, AAEC74B82642E43800C2EFBC /* HistoryStoreTests.swift in Sources */, + 37598EFB2D5A278400720EAF /* ArrayExtensionTests.swift in Sources */, 9FAD623D2BD09DE5007F3A65 /* WebsiteInfoTests.swift in Sources */, 9F180D0F2B69C553000D695F /* Tab+WKUIDelegateTests.swift in Sources */, 4BA1A6E6258C270800F6F690 /* EncryptionKeyGeneratorTests.swift in Sources */, diff --git a/DuckDuckGo/Common/Extensions/ArrayExtension.swift b/DuckDuckGo/Common/Extensions/ArrayExtension.swift index 9972789577..a251c8b3f7 100644 --- a/DuckDuckGo/Common/Extensions/ArrayExtension.swift +++ b/DuckDuckGo/Common/Extensions/ArrayExtension.swift @@ -27,7 +27,7 @@ extension Array { } func chunk(with limit: Int, offset: Int) -> [Element] { - guard !isEmpty, offset < count else { + guard !isEmpty, limit >= 0, offset >= 0, offset < count else { return [] } let endIndex = Swift.min(offset + limit, count) diff --git a/UnitTests/Common/Extensions/ArrayExtensionTests.swift b/UnitTests/Common/Extensions/ArrayExtensionTests.swift new file mode 100644 index 0000000000..65f8c613ef --- /dev/null +++ b/UnitTests/Common/Extensions/ArrayExtensionTests.swift @@ -0,0 +1,58 @@ +// +// ArrayExtensionTests.swift +// +// Copyright © 2025 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +@testable import DuckDuckGo_Privacy_Browser + +final class ArrayExtensionTests: XCTestCase { + + // MARK: - chunk + + func testThatChunkOfEmptyArrayReturnsEmptyArray() { + XCTAssertEqual([Int]().chunk(with: 1, offset: 0), []) + } + + func testThatChunkWithZeroLengthReturnsEmptyArray() { + XCTAssertEqual([1, 2, 3].chunk(with: 0, offset: 0), []) + } + + func testThatChunkWithOffsetOutsideOfBoundsReturnsEmptyArray() { + XCTAssertEqual([1, 2, 3].chunk(with: 1, offset: 100), []) + } + + func testThatChunkWithNegativeOffsetReturnsEmptyArray() { + XCTAssertEqual([1, 2, 3].chunk(with: 1, offset: -5), []) + } + + func testThatChunkWithNegativeLimitReturnsEmptyArray() { + XCTAssertEqual([1, 2, 3].chunk(with: -1, offset: 0), []) + } + + func testThatChunkReturnsPartOfAnArray() { + let array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + XCTAssertEqual(array.chunk(with: 0, offset: 0), []) + XCTAssertEqual(array.chunk(with: 5, offset: 0), [1, 2, 3, 4, 5]) + XCTAssertEqual(array.chunk(with: 5, offset: 1), [2, 3, 4, 5, 6]) + XCTAssertEqual(array.chunk(with: 3, offset: 6), [7, 8, 9]) + } + + func testThatChunkEndingOutsideOfArrayBoundsIsCappedAtArrayEnd() { + XCTAssertEqual([1, 2, 3].chunk(with: 100, offset: 2), [3]) + XCTAssertEqual([1, 2, 3, 4, 5, 6].chunk(with: 4, offset: 4), [5, 6]) + } +} From a45647ae226f7bb6790cfbb876cf23f80afb7cd6 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Mon, 10 Feb 2025 14:23:29 +0100 Subject: [PATCH 5/8] Add HistoryViewCoordinatorTests --- DuckDuckGo-macOS.xcodeproj/project.pbxproj | 6 +++ .../HistoryViewCoordinatorTests.swift | 47 +++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 UnitTests/History/Services/HistoryViewCoordinatorTests.swift diff --git a/DuckDuckGo-macOS.xcodeproj/project.pbxproj b/DuckDuckGo-macOS.xcodeproj/project.pbxproj index dce4ab9d19..4b59549ec8 100644 --- a/DuckDuckGo-macOS.xcodeproj/project.pbxproj +++ b/DuckDuckGo-macOS.xcodeproj/project.pbxproj @@ -1190,6 +1190,8 @@ 37598EF82D5A1D5800720EAF /* HistoryViewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37598EF62D5A1D5500720EAF /* HistoryViewCoordinator.swift */; }; 37598EFA2D5A278400720EAF /* ArrayExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37598EF92D5A278100720EAF /* ArrayExtensionTests.swift */; }; 37598EFB2D5A278400720EAF /* ArrayExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37598EF92D5A278100720EAF /* ArrayExtensionTests.swift */; }; + 37598EFD2D5A33DA00720EAF /* HistoryViewCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37598EFC2D5A33D200720EAF /* HistoryViewCoordinatorTests.swift */; }; + 37598EFE2D5A33DA00720EAF /* HistoryViewCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37598EFC2D5A33D200720EAF /* HistoryViewCoordinatorTests.swift */; }; 376113CC2B29CD5B00E794BB /* CriticalPathsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 565E46DF2B2725DD0013AC2A /* CriticalPathsTests.swift */; }; 376705AF27EB488600DD8D76 /* RoundedSelectionRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0511B3262CAA5A00F6079C /* RoundedSelectionRowView.swift */; }; 376731822C7E226A00EB097B /* HomePageViewBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376731812C7E226A00EB097B /* HomePageViewBackground.swift */; }; @@ -3825,6 +3827,7 @@ 37598EF32D5A18FB00720EAF /* HistoryViewPixel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryViewPixel.swift; sourceTree = ""; }; 37598EF62D5A1D5500720EAF /* HistoryViewCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryViewCoordinator.swift; sourceTree = ""; }; 37598EF92D5A278100720EAF /* ArrayExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayExtensionTests.swift; sourceTree = ""; }; + 37598EFC2D5A33D200720EAF /* HistoryViewCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryViewCoordinatorTests.swift; sourceTree = ""; }; 376113C52B29BCD600E794BB /* SyncE2EUITests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = SyncE2EUITests.xcconfig; sourceTree = ""; }; 376113D42B29CD5B00E794BB /* SyncE2EUITests App Store.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "SyncE2EUITests App Store.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 376113D72B29D0F800E794BB /* SyncE2EUITestsAppStore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = SyncE2EUITestsAppStore.xcconfig; sourceTree = ""; }; @@ -9291,6 +9294,7 @@ AAEC74B02642C48B00C2EFBC /* Services */ = { isa = PBXGroup; children = ( + 37598EFC2D5A33D200720EAF /* HistoryViewCoordinatorTests.swift */, 3745DE072D536EF000024FC8 /* HistoryGroupingProviderTests.swift */, AAEC74B52642CC6A00C2EFBC /* HistoryStoringMock.swift */, AAEC74B72642E43800C2EFBC /* HistoryStoreTests.swift */, @@ -12790,6 +12794,7 @@ 1DB9617B29F1D06D00CF5568 /* InternalUserDeciderMock.swift in Sources */, F1F861172C1B25D4005DB446 /* SubscriptionUIHandlerMock.swift in Sources */, 317295D52AF058D3002C3206 /* MockWaitlistFeatureSetupHandler.swift in Sources */, + 37598EFD2D5A33DA00720EAF /* HistoryViewCoordinatorTests.swift in Sources */, 3706FE49293F661700E42796 /* BookmarkNodePathTests.swift in Sources */, 1DE03425298BC7F000CAB3D7 /* InternalUserDeciderStoreMock.swift in Sources */, 3706FE4A293F661700E42796 /* BookmarkManagedObjectTests.swift in Sources */, @@ -14329,6 +14334,7 @@ B6619F062B17138D00CD9186 /* DataImportSourceViewModelTests.swift in Sources */, 4BBF0917282DD6EF00EE1418 /* TemporaryFileHandlerTests.swift in Sources */, B6A5A27925B93FFF00AA7ADA /* StateRestorationManagerTests.swift in Sources */, + 37598EFE2D5A33DA00720EAF /* HistoryViewCoordinatorTests.swift in Sources */, 9F982F132B822B7B00231028 /* AddEditBookmarkFolderDialogViewModelTests.swift in Sources */, B630E7FE29C887ED00363609 /* NSErrorAdditionalInfo.swift in Sources */, 370270C02C78EB13002E44E4 /* HomePageSettingsModelTests.swift in Sources */, diff --git a/UnitTests/History/Services/HistoryViewCoordinatorTests.swift b/UnitTests/History/Services/HistoryViewCoordinatorTests.swift new file mode 100644 index 0000000000..4078c8b4e9 --- /dev/null +++ b/UnitTests/History/Services/HistoryViewCoordinatorTests.swift @@ -0,0 +1,47 @@ +// +// HistoryViewCoordinatorTests.swift +// +// Copyright © 2025 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import HistoryView +import PixelKit +import XCTest +@testable import DuckDuckGo_Privacy_Browser + +final class HistoryViewCoordinatorTests: XCTestCase { + var coordinator: HistoryViewCoordinator! + var notificationCenter: NotificationCenter! + var firePixelCalls: [PixelKitEvent] = [] + + @MainActor + override func setUp() async throws { + try await super.setUp() + + notificationCenter = NotificationCenter() + firePixelCalls.removeAll() + + coordinator = HistoryViewCoordinator( + historyCoordinator: MockHistoryGroupingDataSource(), + notificationCenter: notificationCenter, + fireDailyPixel: { self.firePixelCalls.append($0) } + ) + } + + func testWhenHistoryViewAppearsThenPixelIsSent() { + notificationCenter.post(name: .historyWebViewDidAppear, object: nil) + XCTAssertEqual(firePixelCalls.count, 1) + } +} From 387691afa5c475ce91192c5736d8e7a13f39157a Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Mon, 10 Feb 2025 14:50:22 +0100 Subject: [PATCH 6/8] Fix deciding policy for history web view --- DuckDuckGo/History/View/HistoryWebViewModel.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/DuckDuckGo/History/View/HistoryWebViewModel.swift b/DuckDuckGo/History/View/HistoryWebViewModel.swift index bfbce7fdec..19fb806b0a 100644 --- a/DuckDuckGo/History/View/HistoryWebViewModel.swift +++ b/DuckDuckGo/History/View/HistoryWebViewModel.swift @@ -62,8 +62,12 @@ final class HistoryWebViewModel: NSObject { } extension HistoryWebViewModel: WKNavigationDelegate { + /// Allow loading all URLs with `duck` scheme and `history` host. + /// Deny all other URLs. func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy { - navigationAction.request.url == .history ? .allow : .cancel + let isDuckScheme = navigationAction.request.url?.isDuckURLScheme == true + let isHistoryHost = navigationAction.request.url?.host == URL.history.host + return (isDuckScheme && isHistoryHost) ? .allow : .cancel } } From 7d8bf1b91607ad96f6fc273be177b592cd19a4b8 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Mon, 10 Feb 2025 15:13:09 +0100 Subject: [PATCH 7/8] Use localizedCaseInsensitiveContains in history search --- DuckDuckGo/History/Services/HistoryViewDataProvider.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo/History/Services/HistoryViewDataProvider.swift b/DuckDuckGo/History/Services/HistoryViewDataProvider.swift index 429333e7bb..8feff107bb 100644 --- a/DuckDuckGo/History/Services/HistoryViewDataProvider.swift +++ b/DuckDuckGo/History/Services/HistoryViewDataProvider.swift @@ -128,7 +128,7 @@ final class HistoryViewDataProvider: HistoryView.DataProviding { case .rangeFilter(let range): return groupings.first(where: { $0.range == range })?.visits ?? [] case .searchTerm(let term): - return visits.filter { $0.title.contains(term) || $0.url.contains(term) } + return visits.filter { $0.title.localizedCaseInsensitiveContains(term) || $0.url.localizedCaseInsensitiveContains(term) } case .domainFilter(let domain): return visits.filter { URL(string: $0.url)?.host == domain } } From 0a85249cccd43adf8767945e3d22ccafa8b2a00e Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Mon, 10 Feb 2025 15:15:09 +0100 Subject: [PATCH 8/8] Fix SwiftLint violation --- DuckDuckGo/History/Services/HistoryViewErrorHandler.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/DuckDuckGo/History/Services/HistoryViewErrorHandler.swift b/DuckDuckGo/History/Services/HistoryViewErrorHandler.swift index 3b16b13050..6b8da240f5 100644 --- a/DuckDuckGo/History/Services/HistoryViewErrorHandler.swift +++ b/DuckDuckGo/History/Services/HistoryViewErrorHandler.swift @@ -35,4 +35,3 @@ final class HistoryViewErrorHandler: EventMapping { fatalError("Use init()") } } -