From 5c141baf96373d84e582e53711df6db39e547ae4 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Fri, 13 Sep 2024 13:57:56 +0500 Subject: [PATCH] Add visted links support (#3261) Task/Issue URL: https://app.asana.com/0/1175293949586521/1204211850543327/f BSK PR: https://github.com/duckduckgo/BrowserServicesKit/pull/992 iOS PR: https://github.com/duckduckgo/iOS/pull/3353 --- DuckDuckGo.xcodeproj/project.pbxproj | 8 +- .../xcshareddata/swiftpm/Package.resolved | 6 +- .../WKVisitedLinkStoreWrapper.swift | 76 +++++++++++++++++++ .../WKWebViewConfigurationExtensions.swift | 12 ++- .../Extensions/WKWebViewExtension.swift | 19 +++++ DuckDuckGo/Fire/Model/Fire.swift | 47 ++++++++++-- DuckDuckGo/Tab/Model/Tab.swift | 2 + .../DataBrokerProtection/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- UnitTests/Fire/Model/FireTests.swift | 41 +++++++++- .../Model/HistoryCoordinatingMock.swift | 4 +- ...wserTabViewControllerOnboardingTests.swift | 6 +- ...ayerOnboardingLocationValidatorTests.swift | 38 +++++++++- 14 files changed, 238 insertions(+), 27 deletions(-) create mode 100644 DuckDuckGo/Common/Extensions/WKVisitedLinkStoreWrapper.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index c9fc340653..04de749aa3 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1777,6 +1777,8 @@ 848648A22C76F4B20082282D /* BookmarksBarMenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848648A02C76F4B20082282D /* BookmarksBarMenuViewController.swift */; }; 84DC715A2C1C1E9000033B8C /* UserDefaultsWrapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DC71582C1C1E8A00033B8C /* UserDefaultsWrapperTests.swift */; }; 84DC715B2C1C1E9000033B8C /* UserDefaultsWrapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DC71582C1C1E8A00033B8C /* UserDefaultsWrapperTests.swift */; }; + 84DDB90A2C92B66E008C997B /* WKVisitedLinkStoreWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DDB9092C92B667008C997B /* WKVisitedLinkStoreWrapper.swift */; }; + 84DDB90B2C92B66E008C997B /* WKVisitedLinkStoreWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DDB9092C92B667008C997B /* WKVisitedLinkStoreWrapper.swift */; }; 84F1C8CF2C7705B500716446 /* BookmarksBarMenuPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F1C8CE2C7705B500716446 /* BookmarksBarMenuPopover.swift */; }; 84F1C8D02C7705B500716446 /* BookmarksBarMenuPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F1C8CE2C7705B500716446 /* BookmarksBarMenuPopover.swift */; }; 84F1C8DE2C774D4200716446 /* NSTableViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F1C8DD2C774D4200716446 /* NSTableViewExtension.swift */; }; @@ -3830,6 +3832,7 @@ 843965142C737022004C8899 /* NSPasteboardExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSPasteboardExtension.swift; sourceTree = ""; }; 848648A02C76F4B20082282D /* BookmarksBarMenuViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksBarMenuViewController.swift; sourceTree = ""; }; 84DC71582C1C1E8A00033B8C /* UserDefaultsWrapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsWrapperTests.swift; sourceTree = ""; }; + 84DDB9092C92B667008C997B /* WKVisitedLinkStoreWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WKVisitedLinkStoreWrapper.swift; sourceTree = ""; }; 84F1C8CE2C7705B500716446 /* BookmarksBarMenuPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksBarMenuPopover.swift; sourceTree = ""; }; 84F1C8DA2C774CA900716446 /* BookmarkListPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkListPopover.swift; sourceTree = ""; }; 84F1C8DD2C774D4200716446 /* NSTableViewExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSTableViewExtension.swift; sourceTree = ""; }; @@ -8230,6 +8233,7 @@ B65C7DFA2B886CF0001E2D5C /* WKPDFHUDViewWrapper.swift */, B645D8F529FA95440024461F /* WKProcessPoolExtension.swift */, AAA0CC69253CC43C0079BC96 /* WKUserContentControllerExtension.swift */, + 84DDB9092C92B667008C997B /* WKVisitedLinkStoreWrapper.swift */, 4B7A60A0273E0BE400BBDFEB /* WKWebsiteDataStoreExtension.swift */, B63D466725BEB6C200874977 /* WKWebView+Private.h */, B63D466825BEB6C200874977 /* WKWebView+SessionState.swift */, @@ -10751,6 +10755,7 @@ 1E559BB22BBCA9F1002B4AF6 /* RedirectNavigationResponder.swift in Sources */, 3706FB6C293F65D500E42796 /* FaviconStore.swift in Sources */, 3706FB6D293F65D500E42796 /* SuggestionListCharacteristics.swift in Sources */, + 84DDB90B2C92B66E008C997B /* WKVisitedLinkStoreWrapper.swift in Sources */, F1C70D7A2BFF50A400599292 /* DataBrokerProtectionLoginItemInterface.swift in Sources */, 377D801F2AB48191002AF251 /* FavoritesDisplayModeSyncHandler.swift in Sources */, 3706FB6F293F65D500E42796 /* BookmarkListViewController.swift in Sources */, @@ -12335,6 +12340,7 @@ B69A14F22B4D6FE800B9417D /* AddBookmarkFolderPopoverViewModel.swift in Sources */, 4BE65478271FCD41008D1D63 /* PasswordManagementNoteItemView.swift in Sources */, AA5C8F632591021700748EB7 /* NSApplicationExtension.swift in Sources */, + 84DDB90A2C92B66E008C997B /* WKVisitedLinkStoreWrapper.swift in Sources */, AA9E9A5625A3AE8400D1959D /* NSWindowExtension.swift in Sources */, 7BD3AF5D2A8E7AF1006F9F56 /* KeychainType+ClientDefault.swift in Sources */, 370A34B12AB24E3700C77F7C /* SyncDebugMenu.swift in Sources */, @@ -14146,7 +14152,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 193.2.1; + version = 194.0.0; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5299ae6ad5..c71eab4b5f 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "branch" : "193.2.1", - "revision" : "e304d397d61f74a43453748bdc86f933e3fe5425" + "branch" : "194.0.0", + "revision" : "026acbd36fb80c95e0bfc6a9080e369dd85db66f" } }, { @@ -75,7 +75,7 @@ { "identity" : "lottie-spm", "kind" : "remoteSourceControl", - "location" : "https://github.com/airbnb/lottie-spm.git", + "location" : "https://github.com/airbnb/lottie-spm", "state" : { "revision" : "1d29eccc24cc8b75bff9f6804155112c0ffc9605", "version" : "4.4.3" diff --git a/DuckDuckGo/Common/Extensions/WKVisitedLinkStoreWrapper.swift b/DuckDuckGo/Common/Extensions/WKVisitedLinkStoreWrapper.swift new file mode 100644 index 0000000000..0dc16c480c --- /dev/null +++ b/DuckDuckGo/Common/Extensions/WKVisitedLinkStoreWrapper.swift @@ -0,0 +1,76 @@ +// +// WKVisitedLinkStoreWrapper.swift +// +// Copyright © 2024 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. +// + +struct WKVisitedLinkStoreWrapper { + + fileprivate let visitedLinkStore: NSObject + + init?(visitedLinkStore: NSObject) { + guard visitedLinkStore.responds(to: Selector.removeVisitedLinkWithURL) else { + assertionFailure("\(visitedLinkStore) does not respond to \(Selector.removeVisitedLinkWithURL)") + return nil + } + guard visitedLinkStore.responds(to: Selector.removeAll) else { + assertionFailure("\(visitedLinkStore) does not respond to \(Selector.removeAll)") + return nil + } + self.visitedLinkStore = visitedLinkStore + } + + @MainActor + func removeVisitedLink(with url: URL) { + visitedLinkStore.perform(Selector.removeVisitedLinkWithURL, with: url as NSURL) + } + + @MainActor + func removeAll() { + visitedLinkStore.perform(Selector.removeAll) + } + + enum Selector { + static let removeAll = NSSelectorFromString("removeAll") + static let removeVisitedLinkWithURL = NSSelectorFromString("removeVisitedLinkWithURL:") + } + +} + +extension WKWebViewConfiguration { + + var visitedLinkStore: WKVisitedLinkStoreWrapper? { + get { + guard self.responds(to: Selector.visitedLinkStore) else { + assertionFailure("WKWebView doesn‘t respond to _visitedLinkStore") + return nil + } + return (self.value(forKey: NSStringFromSelector(Selector.visitedLinkStore)) as? NSObject).flatMap(WKVisitedLinkStoreWrapper.init) + } + set { + guard self.responds(to: Selector.setVisitedLinkStore) else { + assertionFailure("WKWebView doesn‘t respond to _setVisitedLinkStore:") + return + } + self.perform(Selector.setVisitedLinkStore, with: newValue?.visitedLinkStore) + } + } + + enum Selector { + static let visitedLinkStore = NSSelectorFromString("_visitedLinkStore") + static let setVisitedLinkStore = NSSelectorFromString("_setVisitedLinkStore:") + } + +} diff --git a/DuckDuckGo/Common/Extensions/WKWebViewConfigurationExtensions.swift b/DuckDuckGo/Common/Extensions/WKWebViewConfigurationExtensions.swift index 0019081b39..090e9c3121 100644 --- a/DuckDuckGo/Common/Extensions/WKWebViewConfigurationExtensions.swift +++ b/DuckDuckGo/Common/Extensions/WKWebViewConfigurationExtensions.swift @@ -25,13 +25,23 @@ import os.log extension WKWebViewConfiguration { + static var sharedVisitedLinkStore: WKVisitedLinkStoreWrapper? + @MainActor func applyStandardConfiguration(contentBlocking: some ContentBlockingProtocol, burnerMode: BurnerMode, earlyAccessHandlers: [UserScript] = []) { if case .burner(let websiteDataStore) = burnerMode { self.websiteDataStore = websiteDataStore // Fire Window: disable audio/video item info reporting to macOS Control Center / Lock Screen preferences[.mediaSessionEnabled] = false + + } else if let sharedVisitedLinkStore = Self.sharedVisitedLinkStore { + // share visited link store between regular tabs + self.visitedLinkStore = sharedVisitedLinkStore + } else { + // set shared object if not set yet + Self.sharedVisitedLinkStore = self.visitedLinkStore } + allowsAirPlayForMediaPlayback = true if #available(macOS 12.3, *) { preferences.isElementFullscreenEnabled = true @@ -62,7 +72,7 @@ extension WKWebViewConfiguration { self.processPool.geolocationProvider = GeolocationProvider(processPool: self.processPool) _=NSPopover.swizzleShowRelativeToRectOnce - } + } } diff --git a/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift b/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift index 573dfaf38d..7562e4648f 100644 --- a/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift +++ b/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift @@ -348,12 +348,31 @@ extension WKWebView { try await evaluateJavaScript("window.getSelection().removeAllRanges()") as Void? } + var addsVisitedLinks: Bool { + get { + guard self.responds(to: Selector.addsVisitedLinks) else { + assertionFailure("WKWebView doesn‘t respond to _addsVisitedLinks") + return false + } + return self.value(forKey: NSStringFromSelector(Selector.addsVisitedLinks)) as? Bool ?? false + } + set { + guard self.responds(to: Selector.addsVisitedLinks) else { + assertionFailure("WKWebView doesn‘t respond to _setAddsVisitedLinks:") + return + } + self.perform(Selector.setAddsVisitedLinks, with: newValue ? true : nil) + } + } + enum Selector { static let fullScreenPlaceholderView = NSSelectorFromString("_fullScreenPlaceholderView") static let printOperationWithPrintInfoForFrame = NSSelectorFromString("_printOperationWithPrintInfo:forFrame:") static let loadAlternateHTMLString = NSSelectorFromString("_loadAlternateHTMLString:baseURL:forUnreachableURL:") static let mediaMutedState = NSSelectorFromString("_mediaMutedState") static let setPageMuted = NSSelectorFromString("_setPageMuted:") + static let setAddsVisitedLinks = NSSelectorFromString("_setAddsVisitedLinks:") + static let addsVisitedLinks = NSSelectorFromString("_addsVisitedLinks") } } diff --git a/DuckDuckGo/Fire/Model/Fire.swift b/DuckDuckGo/Fire/Model/Fire.swift index 8648e90069..90399af9e4 100644 --- a/DuckDuckGo/Fire/Model/Fire.swift +++ b/DuckDuckGo/Fire/Model/Fire.swift @@ -45,6 +45,7 @@ final class Fire { let tabCleanupPreparer = TabCleanupPreparer() let secureVaultFactory: AutofillVaultFactory let tld: TLD + let getVisitedLinkStore: () -> WKVisitedLinkStoreWrapper? private var dispatchGroup: DispatchGroup? @@ -102,7 +103,8 @@ final class Fire { bookmarkManager: BookmarkManager = LocalBookmarkManager.shared, syncService: DDGSyncing? = nil, syncDataProviders: SyncDataProviders? = nil, - secureVaultFactory: AutofillVaultFactory = AutofillSecureVaultFactory + secureVaultFactory: AutofillVaultFactory = AutofillSecureVaultFactory, + getVisitedLinkStore: (() -> WKVisitedLinkStoreWrapper?)? = nil ) { self.webCacheManager = cacheManager self.historyCoordinating = historyCoordinating @@ -118,6 +120,7 @@ final class Fire { self.syncDataProviders = syncDataProviders ?? NSApp.delegateTyped.syncDataProviders self.secureVaultFactory = secureVaultFactory self.tld = tld + self.getVisitedLinkStore = getVisitedLinkStore ?? { WKWebViewConfiguration.sharedVisitedLinkStore } self.autoconsentManagement = autoconsentManagement ?? AutoconsentManagement.shared if let stateRestorationManager = stateRestorationManager { self.stateRestorationManager = stateRestorationManager @@ -210,7 +213,8 @@ final class Fire { self.burnTabs(burningEntity: .allWindows(mainWindowControllers: windowControllers, selectedDomains: Set())) { Task { @MainActor in await self.burnWebCache() - self.burnHistory { + self.burnAllVisitedLinks() + self.burnAllHistory { self.burnPermissions { self.burnFavicons { self.burnDownloads() @@ -260,6 +264,7 @@ final class Fire { // Convert to eTLD+1 domains domains = domains.convertedToETLDPlus1(tld: tld) + burnVisitedLinks(visits) historyCoordinating.burnVisits(visits) { let entity: BurningEntity @@ -345,30 +350,32 @@ final class Fire { // MARK: - History - private func burnHistory(completion: @escaping () -> Void) { - historyCoordinating.burnAll(completion: completion) - } - @MainActor private func burnHistory(ofEntity entity: BurningEntity, completion: @escaping () -> Void) { let visits: [Visit] switch entity { case .none(selectedDomains: let domains): - burnHistory(of: domains, completion: completion) + burnHistory(of: domains) { urls in + self.burnVisitedLinks(urls) + completion() + } return case .tab(tabViewModel: let tabViewModel, selectedDomains: _, parentTabCollectionViewModel: _): visits = tabViewModel.tab.localHistory case .window(tabCollectionViewModel: let tabCollectionViewModel, selectedDomains: _): visits = tabCollectionViewModel.localHistory + case .allWindows: + burnAllVisitedLinks() burnAllHistory(completion: completion) return } + burnVisitedLinks(visits) historyCoordinating.burnVisits(visits, completion: completion) } - private func burnHistory(of baseDomains: Set, completion: @escaping () -> Void) { + private func burnHistory(of baseDomains: Set, completion: @escaping (Set) -> Void) { historyCoordinating.burnDomains(baseDomains, tld: ContentBlocking.shared.tld, completion: completion) } @@ -376,6 +383,30 @@ final class Fire { historyCoordinating.burnAll(completion: completion) } + // MARK: - Visited links + + @MainActor + private func burnAllVisitedLinks() { + getVisitedLinkStore()?.removeAll() + } + + @MainActor + private func burnVisitedLinks(_ visits: [Visit]) { + guard let visitedLinkStore = getVisitedLinkStore() else { return } + for visit in visits { + guard let url = visit.historyEntry?.url else { continue } + visitedLinkStore.removeVisitedLink(with: url) + } + } + + @MainActor + private func burnVisitedLinks(_ urls: Set) { + guard let visitedLinkStore = getVisitedLinkStore() else { return } + for url in urls { + visitedLinkStore.removeVisitedLink(with: url) + } + } + // MARK: - Zoom levels private func burnZoomLevels() { diff --git a/DuckDuckGo/Tab/Model/Tab.swift b/DuckDuckGo/Tab/Model/Tab.swift index a7ffd60f5b..af8a8e65d1 100644 --- a/DuckDuckGo/Tab/Model/Tab.swift +++ b/DuckDuckGo/Tab/Model/Tab.swift @@ -232,6 +232,8 @@ protocol NewWindowPolicyDecisionMaker { webView = WebView(frame: CGRect(origin: .zero, size: webViewSize), configuration: configuration) webView.allowsLinkPreview = false + webView.addsVisitedLinks = true + permissions = PermissionModel(permissionManager: permissionManager, geolocationService: geolocationService) diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index aec19d02a9..cfdfe1d5ab 100644 --- a/LocalPackages/DataBrokerProtection/Package.swift +++ b/LocalPackages/DataBrokerProtection/Package.swift @@ -29,7 +29,7 @@ let package = Package( targets: ["DataBrokerProtection"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", branch: "193.2.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", branch: "194.0.0"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 0dfebaa384..6ea7a5838b 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -32,7 +32,7 @@ let package = Package( .library(name: "VPNAppLauncher", targets: ["VPNAppLauncher"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "193.2.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "194.0.0"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.3"), .package(path: "../AppLauncher"), .package(path: "../UDSHelper"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 4509ffd71b..d7baf81ac8 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "193.2.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "194.0.0"), .package(path: "../SwiftUIExtensions") ], targets: [ diff --git a/UnitTests/Fire/Model/FireTests.swift b/UnitTests/Fire/Model/FireTests.swift index ef14ca6fc9..8679b79d1f 100644 --- a/UnitTests/Fire/Model/FireTests.swift +++ b/UnitTests/Fire/Model/FireTests.swift @@ -17,6 +17,7 @@ // import Foundation +import History import XCTest import Combine @@ -107,6 +108,7 @@ final class FireTests: XCTestCase { let permissionManager = PermissionManagerMock() let faviconManager = FaviconManagerMock() let recentlyClosedCoordinator = RecentlyClosedCoordinatorMock() + let visitedLinkStore = WKVisitedLinkStoreMock() let fire = Fire(cacheManager: manager, historyCoordinating: historyCoordinator, @@ -115,7 +117,8 @@ final class FireTests: XCTestCase { windowControllerManager: WindowControllersManager.shared, faviconManagement: faviconManager, recentlyClosedCoordinator: recentlyClosedCoordinator, - tld: ContentBlocking.shared.tld) + tld: ContentBlocking.shared.tld, + getVisitedLinkStore: { WKVisitedLinkStoreWrapper(visitedLinkStore: visitedLinkStore) }) let tabCollectionViewModel = TabCollectionViewModel.makeTabCollectionViewModel() _ = WindowsManager.openNewWindow(with: tabCollectionViewModel, lazyLoadTabs: true) @@ -130,6 +133,7 @@ final class FireTests: XCTestCase { XCTAssert(permissionManager.burnPermissionsCalled) XCTAssert(recentlyClosedCoordinator.burnCacheCalled) XCTAssert(zoomLevelsCoordinator.burnAllZoomLevelsCalled) + XCTAssertTrue(visitedLinkStore.removeAllCalled) } @MainActor @@ -221,6 +225,7 @@ final class FireTests: XCTestCase { let permissionManager = PermissionManagerMock() let faviconManager = FaviconManagerMock() let recentlyClosedCoordinator = RecentlyClosedCoordinatorMock() + let visitedLinkStore = WKVisitedLinkStoreMock() let fire = Fire(cacheManager: manager, historyCoordinating: historyCoordinator, @@ -228,7 +233,8 @@ final class FireTests: XCTestCase { windowControllerManager: WindowControllersManager.shared, faviconManagement: faviconManager, recentlyClosedCoordinator: recentlyClosedCoordinator, - tld: ContentBlocking.shared.tld) + tld: ContentBlocking.shared.tld, + getVisitedLinkStore: { WKVisitedLinkStoreWrapper(visitedLinkStore: visitedLinkStore) }) let tabCollectionViewModel = TabCollectionViewModel.makeTabCollectionViewModel() _ = WindowsManager.openNewWindow(with: tabCollectionViewModel, lazyLoadTabs: true) XCTAssertNotEqual(tabCollectionViewModel.allTabsCount, 0) @@ -249,6 +255,7 @@ final class FireTests: XCTestCase { XCTAssert(permissionManager.burnPermissionsOfDomainsCalled) XCTAssertFalse(permissionManager.burnPermissionsCalled) XCTAssert(recentlyClosedCoordinator.burnCacheCalled) + XCTAssertFalse(visitedLinkStore.removeAllCalled) } @MainActor @@ -258,6 +265,7 @@ final class FireTests: XCTestCase { let permissionManager = PermissionManagerMock() let faviconManager = FaviconManagerMock() let recentlyClosedCoordinator = RecentlyClosedCoordinatorMock() + let visitedLinkStore = WKVisitedLinkStoreMock() let fire = Fire(cacheManager: manager, historyCoordinating: historyCoordinator, @@ -265,14 +273,22 @@ final class FireTests: XCTestCase { windowControllerManager: WindowControllersManager.shared, faviconManagement: faviconManager, recentlyClosedCoordinator: recentlyClosedCoordinator, - tld: ContentBlocking.shared.tld) + tld: ContentBlocking.shared.tld, + getVisitedLinkStore: { WKVisitedLinkStoreWrapper(visitedLinkStore: visitedLinkStore) }) let tabCollectionViewModel = TabCollectionViewModel.makeTabCollectionViewModel() _ = WindowsManager.openNewWindow(with: tabCollectionViewModel, lazyLoadTabs: true) XCTAssertNotEqual(tabCollectionViewModel.allTabsCount, 0) let numberOfTabs = tabCollectionViewModel.allTabsCount let finishedBurningExpectation = expectation(description: "Finished burning") - fire.burnVisits(of: [], + let historyEntries = [ + HistoryEntry(identifier: UUID(), url: .duckDuckGo, failedToLoad: false, numberOfTotalVisits: 1, lastVisit: Date(), visits: [], numberOfTrackersBlocked: 0, blockedTrackingEntities: [], trackersFound: false), + HistoryEntry(identifier: UUID(), url: .duckDuckGoEmail, failedToLoad: false, numberOfTotalVisits: 1, lastVisit: Date(), visits: [], numberOfTrackersBlocked: 0, blockedTrackingEntities: [], trackersFound: false), + ] + fire.burnVisits(of: [ + Visit(date: Date(), identifier: nil, historyEntry: historyEntries[0]), + Visit(date: Date(), identifier: nil, historyEntry: historyEntries[1]), + ], except: FireproofDomains.shared, isToday: false, completion: { @@ -287,6 +303,8 @@ final class FireTests: XCTestCase { XCTAssert(permissionManager.burnPermissionsOfDomainsCalled) XCTAssertFalse(permissionManager.burnPermissionsCalled) XCTAssert(recentlyClosedCoordinator.burnCacheCalled) + XCTAssertFalse(visitedLinkStore.removeAllCalled) + XCTAssertEqual(visitedLinkStore.removeVisitedLinkCalledWithURLs, [.duckDuckGo, .duckDuckGoEmail]) } @MainActor @@ -331,3 +349,18 @@ class MockSavedZoomCoordinator: SavedZoomLevelsCoordinating { domainsBurned = baseDomains } } + +private class WKVisitedLinkStoreMock: NSObject { + + private(set) var removeAllCalled = false + @objc func removeAll() { + removeAllCalled = true + } + + private(set) var removeVisitedLinkCalledWithURLs = Set() + @objc(removeVisitedLinkWithURL:) + func removeVisitedLink(with url: URL) { + removeVisitedLinkCalledWithURLs.insert(url) + } + +} diff --git a/UnitTests/History/Model/HistoryCoordinatingMock.swift b/UnitTests/History/Model/HistoryCoordinatingMock.swift index 8996eefda1..5e74455cb6 100644 --- a/UnitTests/History/Model/HistoryCoordinatingMock.swift +++ b/UnitTests/History/Model/HistoryCoordinatingMock.swift @@ -68,9 +68,9 @@ final class HistoryCoordinatingMock: HistoryCoordinating { } var burnDomainsCalled = false - func burnDomains(_ baseDomains: Set, tld: Common.TLD, completion: @escaping () -> Void) { + func burnDomains(_ baseDomains: Set, tld: Common.TLD, completion: @escaping (Set) -> Void) { burnDomainsCalled = true - completion() + completion([]) } var burnVisitsCalled = false diff --git a/UnitTests/Onboarding/ContextualOnboarding/BrowserTabViewControllerOnboardingTests.swift b/UnitTests/Onboarding/ContextualOnboarding/BrowserTabViewControllerOnboardingTests.swift index 62e5162e04..db3eefc81f 100644 --- a/UnitTests/Onboarding/ContextualOnboarding/BrowserTabViewControllerOnboardingTests.swift +++ b/UnitTests/Onboarding/ContextualOnboarding/BrowserTabViewControllerOnboardingTests.swift @@ -28,7 +28,7 @@ final class BrowserTabViewControllerOnboardingTests: XCTestCase { var viewController: BrowserTabViewController! var dialogProvider: MockDialogsProvider! var factory: CapturingDialogFactory! - var tab: Tab! + var tab: DuckDuckGo_Privacy_Browser.Tab! var cancellables: Set = [] let expectation = XCTestExpectation() @@ -37,7 +37,7 @@ final class BrowserTabViewControllerOnboardingTests: XCTestCase { let tabCollectionViewModel = TabCollectionViewModel() dialogProvider = MockDialogsProvider() factory = CapturingDialogFactory(expectation: expectation) - tab = Tab() + tab = DuckDuckGo_Privacy_Browser.Tab() tab.setContent(.url(URL.duckDuckGo, credential: nil, source: .appOpenUrl)) let tabViewModel = TabViewModel(tab: tab) viewController = BrowserTabViewController(tabCollectionViewModel: tabCollectionViewModel, onboardingDialogTypeProvider: dialogProvider, onboardingDialogFactory: factory, featureFlagger: MockFeatureFlagger()) @@ -128,7 +128,7 @@ final class BrowserTabViewControllerOnboardingTests: XCTestCase { class MockDialogsProvider: ContextualOnboardingDialogTypeProviding { var dialog: ContextualDialogType? - func dialogTypeForTab(_ tab: Tab) -> ContextualDialogType? { + func dialogTypeForTab(_ tab: DuckDuckGo_Privacy_Browser.Tab) -> ContextualDialogType? { return dialog } } diff --git a/UnitTests/YoutubePlayer/DuckPlayerOnboardingLocationValidatorTests.swift b/UnitTests/YoutubePlayer/DuckPlayerOnboardingLocationValidatorTests.swift index 09b2329471..720e9f9337 100644 --- a/UnitTests/YoutubePlayer/DuckPlayerOnboardingLocationValidatorTests.swift +++ b/UnitTests/YoutubePlayer/DuckPlayerOnboardingLocationValidatorTests.swift @@ -87,7 +87,41 @@ private final class MockWebView: WKWebView { return mockURL } - override func evaluateJavaScript(_ javaScriptString: String, completionHandler: ((Any?, Error?) -> Void)? = nil) { - completionHandler?(evaluateJavaScriptResult, nil) + convenience init() { + self.init(frame: .zero, configuration: WKWebViewConfiguration()) } + + override init(frame: CGRect, configuration: WKWebViewConfiguration) { + _=Self.swizzleEvaluateJavaScriptOnce + super.init(frame: frame, configuration: configuration) + } + + required init?(coder: NSCoder) { + _=Self.swizzleEvaluateJavaScriptOnce + super.init(coder: coder) + } + +} +private extension WKWebView { + + static let swizzleEvaluateJavaScriptOnce: () = { + guard let originalMethod = class_getInstanceMethod(WKWebView.self, #selector(evaluateJavaScript(_:completionHandler:))), + let swizzledMethod = class_getInstanceMethod(WKWebView.self, #selector(swizzled_evaluateJavaScript(_:completionHandler:))) else { + assertionFailure("Methods not available") + return + } + + method_exchangeImplementations(originalMethod, swizzledMethod) + }() + + // place popover inside bounds of its owner Main Window + @objc(swizzled_evaluateJavaScript:completionHandler:) + private dynamic func swizzled_evaluateJavaScript(_ javaScriptString: String, completionHandler: ((Any?, (any Error)?) -> Void)? = nil) { + if let mockWebView = self as? MockWebView { + completionHandler?(mockWebView.evaluateJavaScriptResult, nil) + return + } + self.swizzled_evaluateJavaScript(javaScriptString, completionHandler: completionHandler) // call the original + } + }