diff --git a/Package.swift b/Package.swift index 84fd312c3..8287eb29b 100644 --- a/Package.swift +++ b/Package.swift @@ -416,6 +416,7 @@ let package = Package( swiftSettings: [ .define("_IS_USER_INITIATED_ENABLED", .when(platforms: [.macOS])), .define("_FRAME_HANDLE_ENABLED", .when(platforms: [.macOS])), + .define("_NAVIGATION_REQUEST_ENABLED", .when(platforms: [.macOS])), .define("PRIVATE_NAVIGATION_DID_FINISH_CALLBACKS_ENABLED", .when(platforms: [.macOS])), .define("_WEBPAGE_PREFS_CUSTOM_HEADERS_ENABLED", .when(platforms: [.macOS])), ], diff --git a/Sources/BookmarksTestDBBuilder/BookmarksTestDBBuilder.swift b/Sources/BookmarksTestDBBuilder/BookmarksTestDBBuilder.swift index 30f4393d2..edca05151 100644 --- a/Sources/BookmarksTestDBBuilder/BookmarksTestDBBuilder.swift +++ b/Sources/BookmarksTestDBBuilder/BookmarksTestDBBuilder.swift @@ -22,7 +22,6 @@ import Persistence import Bookmarks // swiftlint:disable force_try -// swiftlint:disable line_length // swiftlint:disable function_body_length @main @@ -320,5 +319,4 @@ public extension BookmarkEntity { } // swiftlint:enable force_try -// swiftlint:enable line_length // swiftlint:enable function_body_length diff --git a/Sources/BookmarksTestsUtils/BookmarkTree.swift b/Sources/BookmarksTestsUtils/BookmarkTree.swift index 1991dbb65..b1ffe86b3 100644 --- a/Sources/BookmarksTestsUtils/BookmarkTree.swift +++ b/Sources/BookmarksTestsUtils/BookmarkTree.swift @@ -21,7 +21,7 @@ import CoreData import Foundation import XCTest -// swiftlint:disable cyclomatic_complexity function_body_length line_length +// swiftlint:disable cyclomatic_complexity function_body_length public struct ModifiedAtConstraint { var check: (Date?) -> Void @@ -384,4 +384,4 @@ public extension XCTestCase { } } } -// swiftlint:enable cyclomatic_complexity function_body_length line_length +// swiftlint:enable cyclomatic_complexity function_body_length diff --git a/Sources/BrowserServicesKit/ContentScopeScript/UserContentController.swift b/Sources/BrowserServicesKit/ContentScopeScript/UserContentController.swift index 89035701b..6468fcb3f 100644 --- a/Sources/BrowserServicesKit/ContentScopeScript/UserContentController.swift +++ b/Sources/BrowserServicesKit/ContentScopeScript/UserContentController.swift @@ -20,8 +20,6 @@ import WebKit import Combine import UserScript -// swiftlint:disable line_length - public protocol UserContentControllerDelegate: AnyObject { @MainActor func userContentController(_ userContentController: UserContentController, @@ -39,9 +37,9 @@ public protocol UserContentControllerNewContent { var makeUserScripts: @MainActor (SourceProvider) -> UserScripts { get } } -@MainActor final public class UserContentController: WKUserContentController { public let privacyConfigurationManager: PrivacyConfigurationManaging + @MainActor public weak var delegate: UserContentControllerDelegate? public struct ContentBlockingAssets { @@ -62,12 +60,13 @@ final public class UserContentController: WKUserContentController { } } - @Published public private(set) var contentBlockingAssets: ContentBlockingAssets? { + @Published @MainActor public private(set) var contentBlockingAssets: ContentBlockingAssets? { willSet { self.removeAllContentRuleLists() self.removeAllUserScripts() } } + @MainActor private func installContentBlockingAssets(_ contentBlockingAssets: ContentBlockingAssets) { // don‘t install ContentBlockingAssets (especially Message Handlers retaining `self`) after cleanUpBeforeClosing was called guard assetsPublisherCancellable != nil else { return } @@ -83,11 +82,14 @@ final public class UserContentController: WKUserContentController { updateEvent: contentBlockingAssets.updateEvent) } + @MainActor private var localRuleLists = [String: WKContentRuleList]() - + @MainActor private var assetsPublisherCancellable: AnyCancellable? + @MainActor private let scriptMessageHandler = PermanentScriptMessageHandler() + @MainActor public init(assetsPublisher: Pub, privacyConfigurationManager: PrivacyConfigurationManaging) where Pub: Publisher, Content: UserContentControllerNewContent, Pub.Output == Content, Pub.Failure == Never { @@ -113,6 +115,7 @@ final public class UserContentController: WKUserContentController { fatalError("init(coder:) has not been implemented") } + @MainActor private func installGlobalContentRuleLists(_ contentRuleLists: [String: WKContentRuleList]) { guard self.privacyConfigurationManager.privacyConfig.isEnabled(featureKey: .contentBlocking) else { removeAllContentRuleLists() @@ -123,6 +126,7 @@ final public class UserContentController: WKUserContentController { } public struct ContentRulesNotFoundError: Error {} + @MainActor public func enableGlobalContentRuleList(withIdentifier identifier: String) throws { guard let ruleList = self.contentBlockingAssets?.globalRuleLists[identifier] else { throw ContentRulesNotFoundError() @@ -131,6 +135,7 @@ final public class UserContentController: WKUserContentController { } public struct ContentRulesNotEnabledError: Error {} + @MainActor public func disableGlobalContentRuleList(withIdentifier identifier: String) throws { guard let ruleList = self.contentBlockingAssets?.globalRuleLists[identifier] else { throw ContentRulesNotEnabledError() @@ -138,11 +143,13 @@ final public class UserContentController: WKUserContentController { self.remove(ruleList) } + @MainActor public func installLocalContentRuleList(_ ruleList: WKContentRuleList, identifier: String) { localRuleLists[identifier] = ruleList self.add(ruleList) } + @MainActor public func removeLocalContentRuleList(withIdentifier identifier: String) { guard let ruleList = localRuleLists.removeValue(forKey: identifier) else { return @@ -150,16 +157,19 @@ final public class UserContentController: WKUserContentController { self.remove(ruleList) } + @MainActor public override func removeAllContentRuleLists() { localRuleLists = [:] super.removeAllContentRuleLists() } + @MainActor private func installUserScripts(_ wkUserScripts: [WKUserScript], handlers: [UserScript]) { handlers.forEach { self.addHandler($0) } wkUserScripts.forEach(self.addUserScript) } + @MainActor public func cleanUpBeforeClosing() { self.removeAllUserScripts() @@ -175,6 +185,7 @@ final public class UserContentController: WKUserContentController { self.removeAllContentRuleLists() } + @MainActor func addHandler(_ userScript: UserScript) { for messageName in userScript.messageNames { assert(scriptMessageHandler.messageHandler(for: messageName) == nil || type(of: scriptMessageHandler.messageHandler(for: messageName)!) == type(of: userScript), @@ -202,11 +213,13 @@ final public class UserContentController: WKUserContentController { public extension UserContentController { + @MainActor var contentBlockingAssetsInstalled: Bool { contentBlockingAssets != nil } // func awaitContentBlockingAssetsInstalled() async non-retaining `self` + @MainActor var awaitContentBlockingAssetsInstalled: () async -> Void { guard !contentBlockingAssetsInstalled else { return {} } return { [weak self] in @@ -301,5 +314,3 @@ private class PermanentScriptMessageHandler: NSObject, WKScriptMessageHandler, W } } - -// swiftlint:enable line_length diff --git a/Sources/Common/Extensions/URLExtension.swift b/Sources/Common/Extensions/URLExtension.swift index 79294d3fa..d9aa9fec1 100644 --- a/Sources/Common/Extensions/URLExtension.swift +++ b/Sources/Common/Extensions/URLExtension.swift @@ -291,6 +291,11 @@ extension URL { return components.url } + /// returns true if URLs are equal except the #fragment part + public func isSameDocument(_ other: URL) -> Bool { + self.absoluteString.droppingHashedSuffix() == other.absoluteString.droppingHashedSuffix() + } + // MARK: - HTTP/HTTPS public enum URLProtocol: String { diff --git a/Sources/Common/Logging.swift b/Sources/Common/Logging.swift index 1782cd40f..e7bca1df0 100644 --- a/Sources/Common/Logging.swift +++ b/Sources/Common/Logging.swift @@ -105,7 +105,6 @@ extension ProcessInfo { } } -// swiftlint:disable line_length // swiftlint:disable function_parameter_count // MARK: - message first @@ -322,5 +321,4 @@ public func os_log(_ message: @autoclosure () -> String, _ visibility: LogVisibi os_log(.default, log: .default, message(), visibility) } -// swiftlint:enable line_length // swiftlint:enable function_parameter_count diff --git a/Sources/Common/TimeoutError.swift b/Sources/Common/TimeoutError.swift index 9c733b99f..a19a1836a 100644 --- a/Sources/Common/TimeoutError.swift +++ b/Sources/Common/TimeoutError.swift @@ -28,7 +28,6 @@ public struct TimeoutError: Error, LocalizedError, CustomDebugStringConvertible public let line: UInt public var errorDescription: String? { - // swiftlint:disable:next line_length "TimeoutError(started: \(date), \(interval != nil ? "timeout: \(interval!)s, " : "")\(description != nil ? " description: " + description! : "") at \(file):\(line))" } diff --git a/Sources/DDGSync/DDGSync.swift b/Sources/DDGSync/DDGSync.swift index 81d1903f9..9d560056d 100644 --- a/Sources/DDGSync/DDGSync.swift +++ b/Sources/DDGSync/DDGSync.swift @@ -334,7 +334,6 @@ public class DDGSync: DDGSyncing { try updateAccount(nil) throw SyncError.unauthenticatedWhileLoggedIn } catch { - // swiftlint:disable:next line_length os_log(.error, log: dependencies.log, "Failed to delete account upon unauthenticated server response: %{public}s", error.localizedDescription) if error is SyncError { throw error diff --git a/Sources/DDGSync/internal/SyncQueue.swift b/Sources/DDGSync/internal/SyncQueue.swift index a5a2bd4da..fb6e4d22f 100644 --- a/Sources/DDGSync/internal/SyncQueue.swift +++ b/Sources/DDGSync/internal/SyncQueue.swift @@ -109,7 +109,6 @@ final class SyncQueue { try dataProvider.prepareForFirstSync() try dataProvider.registerFeature(withState: setupState) } catch { - // swiftlint:disable:next line_length os_log(.debug, log: self.log, "Error when preparing %{public}s for first sync: %{public}s", dataProvider.feature.name, error.localizedDescription) dataProvider.handleSyncError(error) throw error diff --git a/Sources/Navigation/DistributedNavigationDelegate.swift b/Sources/Navigation/DistributedNavigationDelegate.swift index 90dfcf7d7..25caae60b 100644 --- a/Sources/Navigation/DistributedNavigationDelegate.swift +++ b/Sources/Navigation/DistributedNavigationDelegate.swift @@ -121,7 +121,7 @@ private extension DistributedNavigationDelegate { // cancel the decision making Task if WebView deallocates before it‘s finished let webViewDeinitObserver = webView.deinitObservers.insert(NSObject.DeinitObserver()).memberAfterInsert - let webViewDebugRef = Unmanaged.passUnretained(webView).toOpaque() + let webViewDebugRef = Unmanaged.passUnretained(webView).toOpaque().hexValue // TO DO: ideally the Task should be executed synchronously until the first await, check it later when custom Executors arrive to Swift let task = Task.detached { @MainActor [responders, weak webView, weak webViewDeinitObserver] in @@ -131,7 +131,7 @@ private extension DistributedNavigationDelegate { guard !Task.isCancelled else { return } #if DEBUG let longDecisionMakingCheckCancellable = Self.checkLongDecisionMaking(performedBy: responder) { - ": " + actionDebugInfo.debugDescription + ": " + actionDebugInfo.debugDescription } defer { longDecisionMakingCheckCancellable?.cancel() @@ -164,7 +164,7 @@ private extension DistributedNavigationDelegate { // cancel the Task if WebView deallocates before it‘s finished webViewDeinitObserver.onDeinit { [log] in - os_log("cancelling \(actionDebugInfo.debugDescription) decision making due to deallocation", log: log, type: .error) + os_log("cancelling \(actionDebugInfo.debugDescription) decision making due to deallocation", log: log, type: .error) task.cancel() } @@ -240,11 +240,19 @@ private extension DistributedNavigationDelegate { let wkNavigation = webView.expectedMainFrameNavigation(for: wkNavigationAction) #endif let navigation: Navigation = { - if let navigation = wkNavigation?.navigation, - // same-document NavigationActions have a previous WKNavigation mainFrameNavigation - !wkNavigationAction.isSameDocumentNavigation { - // it‘s a server-redirect or a developer-initiated navigation, so the WKNavigation already has an associated Navigation object - return navigation + if let navigation = wkNavigation?.navigation { + // same-document NavigationActions have a previous WKNavigation mainFrameNavigation + // use the same Navigation for finished navigations (as they won‘t receive `didFinish`) + // but create a new Navigation and client-redirect an old one for non-finished navigations + if wkNavigationAction.isSameDocumentNavigation { + if navigation === startedNavigation, !navigation.state.isFinished { + navigation.willPerformClientRedirect(to: wkNavigationAction.request.url ?? .empty, delay: 0) + } + // continue to new Navigation object creation + } else { + // it‘s a server-redirect or a developer-initiated navigation, so the WKNavigation already has an associated Navigation object + return navigation + } // server-redirected navigation continues with the same WKNavigation identity } else if let startedNavigation, @@ -259,7 +267,7 @@ private extension DistributedNavigationDelegate { return Navigation(identity: NavigationIdentity(wkNavigation), responders: responders, state: .expected(nil), isCurrent: false) }() // wkNavigation.navigation = navigation - if wkNavigation?.navigation == nil { + if wkNavigation?.navigation == nil || wkNavigation?.navigation?.state.isFinished == true { navigation.associate(with: wkNavigation) } @@ -372,6 +380,9 @@ extension DistributedNavigationDelegate: WKNavigationDelegate { if let mainFrameNavigation, !mainFrameNavigation.isCurrent { // another navigation is starting self.startedNavigation?.didResignCurrent() + guard case .navigationActionReceived = mainFrameNavigation.state else { + return // navigation cancelled + } self.willStart(mainFrameNavigation) } @@ -428,7 +439,12 @@ extension DistributedNavigationDelegate: WKNavigationDelegate { private func willStart(_ navigation: Navigation) { os_log("willStart %s", log: log, type: .default, navigation.debugDescription) - if case .redirect(.client) = navigation.navigationAction.navigationType { + if navigation.navigationAction.navigationType.redirect?.isClient == true // is client redirect? + // is same document navigation received as client redirect? + || (startedNavigation !== navigation + && navigation.navigationAction.navigationType == .sameDocumentNavigation(.anchorNavigation) + && startedNavigation?.url.isSameDocument(navigation.url) == true) { + // notify the original (redirected) Navigation about the redirect NavigationAction received // this should call the overriden ResponderChain inside `willPerformClientRedirect` // that in turn notifies the original responders and finishes the Navigation @@ -439,7 +455,14 @@ extension DistributedNavigationDelegate: WKNavigationDelegate { for responder in navigation.navigationResponders { responder.willStart(navigation) } - navigationExpectedToStart = navigation + // same-document navigations won‘t receive didStartProvisionalNavigation, so change the state here + if navigation.navigationAction.navigationType.isSameDocumentNavigation { + navigation.started(nil) + startedNavigation = navigation + navigationExpectedToStart = nil + } else { + navigationExpectedToStart = navigation + } } @MainActor @@ -790,18 +813,85 @@ extension DistributedNavigationDelegate: WKNavigationDelegate { #if PRIVATE_NAVIGATION_DID_FINISH_CALLBACKS_ENABLED @MainActor @objc(_webView:navigation:didSameDocumentNavigation:) - public func webView(_ webView: WKWebView, wkNavigation: WKNavigation?, didSameDocumentNavigation navigationType: Int) { - os_log("didSameDocumentNavigation %s: %d", log: log, type: .default, wkNavigation.debugDescription, navigationType) - + public func webView(_ webView: WKWebView, wkNavigation: WKNavigation?, didSameDocumentNavigation wkNavigationType: Int) { // currentHistoryItemIdentity should only change for completed navigation, not while in progress - let navigationType = WKSameDocumentNavigationType(rawValue: navigationType) - if case .anchorNavigation = navigationType { - updateCurrentHistoryItemIdentity(webView.backForwardList.currentItem) + let navigationType = WKSameDocumentNavigationType(rawValue: wkNavigationType) ?? { + assertionFailure("Unsupported SameDocumentNavigationType \(wkNavigationType)") + return .anchorNavigation + }() + + // + // Anchor navigations are using the original WKNavigation object that was used to load current document, + // but its `.request` will contain an updated URL with a #fragment. + // + // - These navigations go through the standard `decidePolicyFor` and `willStart` sequence and stored + // in the `startedNavigation` var. Such navigations have their `isCurrent` flag set. + // - In case of Anchor navigations there‘s a preceding State Pop event received, it‘s probably used + // to manage navigation history and is not really of our interest. + // Those navigations won‘t have `isCurrent` flag set (see below) + // + // Session State push/replace/pop navigations don‘t receive `decidePolicyFor` but their WKNavigation (new one) contains a valid request. + // + // - In case of a Session State Pop navigation, an additional Anchor Navigation event will be received. + // Its WKNavigation is set to the original document load navigation (finished long before). + // Such navigations have `isCurrent` unset when the original document has loaded allowing us to distinguish + // the real State Pop events + // + let navigation: Navigation + if let associatedNavigation = wkNavigation?.navigation { + // Anchor navigations will have an associated Navigation set in `decidePolicyFor` + if let startedNavigation, startedNavigation.identity == associatedNavigation.identity { + // client-redirect to the same document - same WKNavigation (identity) is used for different Navigation object + // so instead use `startedNavigation` set in `willStart` + navigation = startedNavigation + } else { + navigation = associatedNavigation + } + // mark Navigation as finished as we‘re in __did__SameDocumentNavigation + // if we‘ve got the main-document load Navigation (which may be the case) - we don‘t want to finish it here. + if navigation.isCurrent, navigation.navigationAction.navigationType.isSameDocumentNavigation, !navigation.isCompleted { + navigation.didFinish() + } + + } else { + // don‘t mark extra Session State Pop navigations as `current` when there‘s a `current` Anchor navigation stored in `startedNavigation` + let isCurrent = if let startedNavigation { + !(startedNavigation.navigationAction.navigationType.isSameDocumentNavigation && startedNavigation.isCurrent) + } else { + true + } + + assert(wkNavigation != nil) + navigation = Navigation(identity: NavigationIdentity(wkNavigation), responders: responders, state: .expected(nil), isCurrent: isCurrent) + let request = wkNavigation?.request ?? URLRequest(url: webView.url ?? .empty) + let navigationAction = NavigationAction(request: request, navigationType: .sameDocumentNavigation(navigationType), currentHistoryItemIdentity: currentHistoryItemIdentity, redirectHistory: nil, isUserInitiated: wkNavigation?.isUserInitiated ?? false, sourceFrame: .mainFrame(for: webView), targetFrame: .mainFrame(for: webView), shouldDownload: false, mainFrameNavigation: navigation) + navigation.navigationActionReceived(navigationAction) + os_log("new same-doc navigation(.%d): %s (%s): %s, isCurrent: %d", log: log, type: .debug, wkNavigationType, wkNavigation.debugDescription, navigation.debugDescription, navigationAction.debugDescription, isCurrent ? 1 : 0) + + // store `current` navigations in `startedNavigation` to get `currentNavigation` published + if isCurrent { + self.startedNavigation = navigation + } + // mark Navigation as finished as we‘re in __did__SameDocumentNavigation + navigation.didFinish() } + os_log("didSameDocumentNavigation: %s.%s: %s", log: log, type: .default, wkNavigation.debugDescription, navigationType.debugDescription, navigation.debugDescription) + for responder in responders { - responder.navigation(wkNavigation?.navigation, didSameDocumentNavigationOf: navigationType) + responder.navigation(navigation, didSameDocumentNavigationOf: navigationType) } + + // same as above, main-document load navigations sometimes passed to this method shouldn‘t have `isCurrent` unset + if navigation.navigationAction.navigationType.isSameDocumentNavigation { + if self.startedNavigation === navigation { + self.startedNavigation = nil // will call `didResignCurrent` + } else { + navigation.didResignCurrent() + } + } + + updateCurrentHistoryItemIdentity(webView.backForwardList.currentItem) } @MainActor diff --git a/Sources/Navigation/Extensions/WKNavigationActionExtension.swift b/Sources/Navigation/Extensions/WKNavigationActionExtension.swift index 176535cc1..56b35eb94 100644 --- a/Sources/Navigation/Extensions/WKNavigationActionExtension.swift +++ b/Sources/Navigation/Extensions/WKNavigationActionExtension.swift @@ -19,7 +19,6 @@ import Common import WebKit -// swiftlint:disable line_length extension WKNavigationAction: WebViewNavigationAction { /// Safe Optional `sourceFrame: WKFrameInfo` getter: @@ -168,17 +167,17 @@ extension WKNavigationAction: WebViewNavigationAction { #endif public var isSameDocumentNavigation: Bool { - guard let currentURL = targetFrame?.safeRequest?.url?.absoluteString, - let newURL = self.request.url?.absoluteString, + guard let currentURL = targetFrame?.safeRequest?.url, + let newURL = self.request.url, !currentURL.isEmpty, !newURL.isEmpty else { return false } switch navigationType { case .linkActivated, .other: - return self.isRedirect != true && newURL.hashedSuffix != nil && currentURL.droppingHashedSuffix() == newURL.droppingHashedSuffix() + return self.isRedirect != true && newURL.absoluteString.hashedSuffix != nil && currentURL.isSameDocument(newURL) case .backForward: - return (newURL.hashedSuffix != nil || currentURL.hashedSuffix != nil) && currentURL.droppingHashedSuffix() == newURL.droppingHashedSuffix() + return (newURL.absoluteString.hashedSuffix != nil || currentURL.absoluteString.hashedSuffix != nil) && currentURL.isSameDocument(newURL) case .reload, .formSubmitted, .formResubmitted: return false @unknown default: @@ -197,4 +196,3 @@ extension WKNavigationActionPolicy { }() } -// swiftlint:enable line_length diff --git a/Sources/Navigation/Extensions/WKNavigationExtension.swift b/Sources/Navigation/Extensions/WKNavigationExtension.swift index d0c0ebd6b..fd0552e3a 100644 --- a/Sources/Navigation/Extensions/WKNavigationExtension.swift +++ b/Sources/Navigation/Extensions/WKNavigationExtension.swift @@ -31,4 +31,29 @@ extension WKNavigation { } } + open override func value(forUndefinedKey key: String) -> Any? { + assertionFailure("valueForUndefinedKey: \(key)") + return nil + } + +#if _IS_USER_INITIATED_ENABLED + @nonobjc public var isUserInitiated: Bool? { + return self.value(forKey: "isUserInitiated") as? Bool + } +#else + public var isUserInitiated: Bool? { + return nil + } +#endif + +#if _NAVIGATION_REQUEST_ENABLED + internal var request: URLRequest? { + self.value(forKey: "request") as? URLRequest + } +#else + internal var request: URLRequest? { + return nil + } +#endif + } diff --git a/Sources/Navigation/Extensions/WKWebViewExtension.swift b/Sources/Navigation/Extensions/WKWebViewExtension.swift index 568a3142a..454d44df2 100644 --- a/Sources/Navigation/Extensions/WKWebViewExtension.swift +++ b/Sources/Navigation/Extensions/WKWebViewExtension.swift @@ -18,8 +18,6 @@ import WebKit -// swiftlint:disable line_length -// swiftlint:disable trailing_comma extension WKWebView { #if _FRAME_HANDLE_ENABLED @@ -159,6 +157,3 @@ extension WKNavigation { } } #endif - -// swiftlint:enable line_length -// swiftlint:enable trailing_comma diff --git a/Sources/Navigation/FrameInfo.swift b/Sources/Navigation/FrameInfo.swift index 55213169c..5142c19f4 100644 --- a/Sources/Navigation/FrameInfo.swift +++ b/Sources/Navigation/FrameInfo.swift @@ -20,7 +20,6 @@ import Common import Foundation import WebKit -// swiftlint:disable line_length public struct FrameInfo { public weak var webView: WKWebView? @@ -100,4 +99,3 @@ extension FrameInfo: CustomDebugStringConvertible { return "" } } -// swiftlint:enable line_length diff --git a/Sources/Navigation/Navigation.swift b/Sources/Navigation/Navigation.swift index 6850c8146..358586877 100644 --- a/Sources/Navigation/Navigation.swift +++ b/Sources/Navigation/Navigation.swift @@ -20,7 +20,6 @@ import Common import Foundation import WebKit -// swiftlint:disable line_length @MainActor public final class Navigation { @@ -291,6 +290,8 @@ extension Navigation { case .responseReceived: // regular flow self.state = .finished + case .navigationActionReceived where navigationAction.navigationType.isSameDocumentNavigation: + self.state = .finished case .expected, .navigationActionReceived, .approved, .finished, .failed: assertionFailure("unexpected state \(self.state)") } @@ -381,7 +382,7 @@ extension Navigation { extension Navigation: CustomDebugStringConvertible { public var debugDescription: String { - "<\(identity) #\(navigationAction.identifier): url:\(url.absoluteString) state:\(state)\(isCommitted ? "(committed)" : "") type:\(navigationActions.last?.navigationType.debugDescription ?? "")>" + "<\(identity) #\(navigationAction.identifier): url:\(url.absoluteString) state:\(state)\(isCommitted ? "(committed)" : "") type:\(navigationActions.last?.navigationType.debugDescription ?? "")\(isCurrent ? "" : " non-current")>" } } @@ -390,4 +391,3 @@ extension NavigationIdentity: CustomStringConvertible { "WKNavigation: " + (value?.hexValue ?? "nil") } } -// swiftlint:enable line_length diff --git a/Sources/Navigation/NavigationAction.swift b/Sources/Navigation/NavigationAction.swift index 6963c0a1a..e0c985e8a 100644 --- a/Sources/Navigation/NavigationAction.swift +++ b/Sources/Navigation/NavigationAction.swift @@ -20,8 +20,6 @@ import Common import Foundation import WebKit -// swiftlint:disable line_length - public struct MainFrame { fileprivate init() {} } @@ -274,7 +272,12 @@ extension NavigationAction: CustomDebugStringConvertible { #else let sourceFrame = sourceFrame.debugDescription + " -> " #endif - return "" +#if PRIVATE_NAVIGATION_DID_FINISH_CALLBACKS_ENABLED + let fromHistoryItem = fromHistoryItemIdentity != nil ? " from: " + fromHistoryItemIdentity!.debugDescription : "" +#else + let fromHistoryItem = "" +#endif + return "" } } @@ -295,4 +298,16 @@ extension NavigationPreferences: CustomDebugStringConvertible { } } -// swiftlint:enable line_length +extension HistoryItemIdentity: CustomDebugStringConvertible { + public var debugDescription: String { + "\(object)".replacingOccurrences(of: "WKBackForwardListItem: ", with: "").dropping(suffix: ">") + + { + guard let backForwardListItem = object as? WKBackForwardListItem else { return "" } + var url = " " + backForwardListItem.url.absoluteString + if backForwardListItem.initialURL != backForwardListItem.url { + url += " (initial: \(backForwardListItem.initialURL.absoluteString))" + } + return url + }() + ">" + } +} diff --git a/Sources/Navigation/NavigationResponder.swift b/Sources/Navigation/NavigationResponder.swift index 044be5c3d..0228b8d39 100644 --- a/Sources/Navigation/NavigationResponder.swift +++ b/Sources/Navigation/NavigationResponder.swift @@ -98,7 +98,7 @@ public protocol NavigationResponder { // MARK: - Private #if PRIVATE_NAVIGATION_DID_FINISH_CALLBACKS_ENABLED @MainActor - func navigation(_ navigation: Navigation?, didSameDocumentNavigationOf navigationType: WKSameDocumentNavigationType?) + func navigation(_ navigation: Navigation, didSameDocumentNavigationOf navigationType: WKSameDocumentNavigationType) @MainActor func webViewWillPerformClientRedirect(to url: URL, withDelay delay: TimeInterval) @@ -158,7 +158,7 @@ public extension NavigationResponder { #if PRIVATE_NAVIGATION_DID_FINISH_CALLBACKS_ENABLED @MainActor - func navigation(_ navigation: Navigation?, didSameDocumentNavigationOf navigationType: WKSameDocumentNavigationType?) {} + func navigation(_ navigation: Navigation, didSameDocumentNavigationOf navigationType: WKSameDocumentNavigationType) {} @MainActor func didFinishLoad(with request: URLRequest, in frame: WKFrameInfo) {} diff --git a/Sources/Navigation/NavigationResponse.swift b/Sources/Navigation/NavigationResponse.swift index c82e86bae..29a055c26 100644 --- a/Sources/Navigation/NavigationResponse.swift +++ b/Sources/Navigation/NavigationResponse.swift @@ -19,7 +19,6 @@ import Foundation import WebKit -// swiftlint:disable line_length public struct NavigationResponse { public let response: URLResponse @@ -85,5 +84,3 @@ extension NavigationResponsePolicy? { /// Pass decision making to next responder public static let next = NavigationResponsePolicy?.none } - -// swiftlint:enable line_length diff --git a/Sources/Navigation/NavigationType.swift b/Sources/Navigation/NavigationType.swift index 5880de8e2..a0b67e3c4 100644 --- a/Sources/Navigation/NavigationType.swift +++ b/Sources/Navigation/NavigationType.swift @@ -40,7 +40,12 @@ public enum NavigationType: Equatable { case redirect(RedirectType) case sessionRestoration case alternateHtmlLoad + +#if PRIVATE_NAVIGATION_DID_FINISH_CALLBACKS_ENABLED + case sameDocumentNavigation(WKSameDocumentNavigationType) +#else case sameDocumentNavigation +#endif case other @@ -51,7 +56,7 @@ public enum NavigationType: Equatable { switch navigationAction.navigationType { case .linkActivated where navigationAction.isSameDocumentNavigation, .other where navigationAction.isSameDocumentNavigation: - self = .sameDocumentNavigation + self = .sameDocumentNavigation(.anchorNavigation) case .linkActivated: #if os(macOS) @@ -137,6 +142,11 @@ public extension NavigationType { return false } + var isSameDocumentNavigation: Bool { + if case .sameDocumentNavigation = self { return true } + return false + } + } public protocol WebViewNavigationAction { @@ -191,10 +201,26 @@ extension NavigationType: CustomDebugStringConvertible { case .other: return "other" case .redirect(let redirect): return "redirect(\(redirect))" +#if PRIVATE_NAVIGATION_DID_FINISH_CALLBACKS_ENABLED + case .sameDocumentNavigation(let navigationType): + return "sameDocumentNavigation(\(navigationType.debugDescription))" +#else case .sameDocumentNavigation: return "sameDocumentNavigation" +#endif case .custom(let name): return "custom(\(name.rawValue))" } } } + +extension WKSameDocumentNavigationType: CustomDebugStringConvertible { + public var debugDescription: String { + switch self { + case .anchorNavigation: "anchorNavigation" + case .sessionStatePush: "sessionStatePush" + case .sessionStateReplace: "sessionStateReplace" + case .sessionStatePop: "sessionStatePop" + } + } +} diff --git a/Sources/Navigation/Navigator.swift b/Sources/Navigation/Navigator.swift index fd2fa58bf..04147968b 100644 --- a/Sources/Navigation/Navigator.swift +++ b/Sources/Navigation/Navigator.swift @@ -19,7 +19,6 @@ import Foundation import WebKit -// swiftlint:disable line_length @MainActor public struct Navigator { @@ -181,4 +180,3 @@ extension WKWebView { } } -// swiftlint:enable line_length diff --git a/Sources/NetworkProtection/Logging/Logging.swift b/Sources/NetworkProtection/Logging/Logging.swift index 0a6c03825..5563844c8 100644 --- a/Sources/NetworkProtection/Logging/Logging.swift +++ b/Sources/NetworkProtection/Logging/Logging.swift @@ -74,7 +74,6 @@ extension OSLog { } } -// swiftlint:disable line_length struct Logging { static let subsystem = "com.duckduckgo.macos.browser.network-protection" @@ -118,4 +117,3 @@ struct Logging { fileprivate static let networkProtectionEntitlementLoggingEnabled = true fileprivate static let networkProtectionEntitlementLog: OSLog = OSLog(subsystem: subsystem, category: "Network Protection: Entitlement Monitor") } -// swiftlint:enable line_length diff --git a/Sources/NetworkProtection/PacketTunnelProvider.swift b/Sources/NetworkProtection/PacketTunnelProvider.swift index bea266fa4..4ff900ba0 100644 --- a/Sources/NetworkProtection/PacketTunnelProvider.swift +++ b/Sources/NetworkProtection/PacketTunnelProvider.swift @@ -495,8 +495,6 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { resetIssueStateOnTunnelStart(startupOptions) - let startTime = DispatchTime.now() - let internalCompletionHandler = { [weak self] (error: Error?) in guard let self else { completionHandler(error) diff --git a/Sources/NetworkProtection/Status/ConnectionStatusObserver/ConnectionStatusObserverThroughDistributedNotifications.swift b/Sources/NetworkProtection/Status/ConnectionStatusObserver/ConnectionStatusObserverThroughDistributedNotifications.swift index 4c82af171..b85c29d37 100644 --- a/Sources/NetworkProtection/Status/ConnectionStatusObserver/ConnectionStatusObserverThroughDistributedNotifications.swift +++ b/Sources/NetworkProtection/Status/ConnectionStatusObserver/ConnectionStatusObserverThroughDistributedNotifications.swift @@ -36,7 +36,6 @@ public class ConnectionStatusObserverThroughDistributedNotifications: Connection // MARK: - Network Path Monitoring - // swiftlint:disable:next line_length private static let monitorDispatchQueue = DispatchQueue(label: "com.duckduckgo.NetworkProtection.ConnectionStatusObserverThroughDistributedNotifications.monitorDispatchQueue", qos: .background) private let monitor = NWPathMonitor() private static let timeoutOnNetworkChanges: TimeInterval = .seconds(3) diff --git a/Sources/NetworkProtectionTestUtils/FeatureActivation/NetworkProtectionCodeRedemptionCoordinatorTestExtensions.swift b/Sources/NetworkProtectionTestUtils/FeatureActivation/NetworkProtectionCodeRedemptionCoordinatorTestExtensions.swift index 8642e7628..0a6a42239 100644 --- a/Sources/NetworkProtectionTestUtils/FeatureActivation/NetworkProtectionCodeRedemptionCoordinatorTestExtensions.swift +++ b/Sources/NetworkProtectionTestUtils/FeatureActivation/NetworkProtectionCodeRedemptionCoordinatorTestExtensions.swift @@ -35,7 +35,6 @@ public extension NetworkProtectionCodeRedemptionCoordinator { return NetworkProtectionCodeRedemptionCoordinator(networkClient: client, tokenStore: tokenStore, errorEvents: errorEvents) } - // swiftlint:disable:next line_length class func whereRedeemFails(returning error: NetworkProtectionClientError = .failedToEncodeRedeemRequest) -> NetworkProtectionCodeRedemptionCoordinator { let client = MockNetworkProtectionClient() client.stubRedeem = .failure(error) diff --git a/Sources/NetworkProtectionTestUtils/Networking/MockNetworkProtectionClient.swift b/Sources/NetworkProtectionTestUtils/Networking/MockNetworkProtectionClient.swift index 5f60d31d9..ae178f292 100644 --- a/Sources/NetworkProtectionTestUtils/Networking/MockNetworkProtectionClient.swift +++ b/Sources/NetworkProtectionTestUtils/Networking/MockNetworkProtectionClient.swift @@ -19,7 +19,6 @@ import Foundation @testable import NetworkProtection -// swiftlint:disable line_length public final class MockNetworkProtectionClient: NetworkProtectionClient { public init() { } @@ -85,4 +84,3 @@ public final class MockNetworkProtectionClient: NetworkProtectionClient { return stubRegister } } -// swiftlint:enable line_length diff --git a/Sources/Subscription/AccountManager.swift b/Sources/Subscription/AccountManager.swift index 4c2e376c9..cef610444 100644 --- a/Sources/Subscription/AccountManager.swift +++ b/Sources/Subscription/AccountManager.swift @@ -181,7 +181,7 @@ public class AccountManager: AccountManaging { public func migrateAccessTokenToNewStore() throws { var errorToThrow: Error? do { - if let newAccessToken = try accessTokenStorage.getAccessToken() { + if try accessTokenStorage.getAccessToken() != nil { errorToThrow = MigrationError.noMigrationNeeded } else if let oldAccessToken = try storage.getAccessToken() { try accessTokenStorage.store(accessToken: oldAccessToken) diff --git a/Sources/SyncDataProviders/Bookmarks/BookmarksProvider.swift b/Sources/SyncDataProviders/Bookmarks/BookmarksProvider.swift index d63e9f691..544981198 100644 --- a/Sources/SyncDataProviders/Bookmarks/BookmarksProvider.swift +++ b/Sources/SyncDataProviders/Bookmarks/BookmarksProvider.swift @@ -29,7 +29,6 @@ public struct FaviconsFetcherInput { public var deletedBookmarksUUIDs: Set } -// swiftlint:disable line_length public final class BookmarksProvider: DataProvider { public private(set) var faviconsFetcherInput: FaviconsFetcherInput = .init(modifiedBookmarksUUIDs: [], deletedBookmarksUUIDs: []) @@ -239,4 +238,3 @@ public final class BookmarksProvider: DataProvider { #endif } -// swiftlint:enable line_length diff --git a/Tests/BrowserServicesKitTests/ContentBlocker/ContentBlockerRulesManagerMultipleRulesTests.swift b/Tests/BrowserServicesKitTests/ContentBlocker/ContentBlockerRulesManagerMultipleRulesTests.swift index caa6b1199..d02b47599 100644 --- a/Tests/BrowserServicesKitTests/ContentBlocker/ContentBlockerRulesManagerMultipleRulesTests.swift +++ b/Tests/BrowserServicesKitTests/ContentBlocker/ContentBlockerRulesManagerMultipleRulesTests.swift @@ -22,7 +22,6 @@ import BrowserServicesKit import WebKit import Common -// swiftlint:disable unused_closure_parameter class ContentBlockerRulesManagerMultipleRulesTests: ContentBlockerRulesManagerTests { let firstRules = """ @@ -353,4 +352,3 @@ class ContentBlockerRulesManagerMultipleRulesTests: ContentBlockerRulesManagerTe } } -// swiftlint:enable unused_closure_parameter diff --git a/Tests/NavigationTests/ClosureNavigationResponderTests.swift b/Tests/NavigationTests/ClosureNavigationResponderTests.swift index 882679ff0..9a1478fd4 100644 --- a/Tests/NavigationTests/ClosureNavigationResponderTests.swift +++ b/Tests/NavigationTests/ClosureNavigationResponderTests.swift @@ -25,9 +25,6 @@ import WebKit import XCTest @testable import Navigation -// swiftlint:disable unused_closure_parameter -// swiftlint:disable opening_brace - @available(macOS 12.0, iOS 15.0, *) class ClosureNavigationResponderTests: DistributedNavigationDelegateTestsBase { @@ -434,7 +431,4 @@ class ClosureNavigationResponderTests: DistributedNavigationDelegateTestsBase { } -// swiftlint:enable unused_closure_parameter -// swiftlint:enable opening_brace - #endif diff --git a/Tests/NavigationTests/DistributedNavigationDelegateTests.swift b/Tests/NavigationTests/DistributedNavigationDelegateTests.swift index f90793b20..f93206b3b 100644 --- a/Tests/NavigationTests/DistributedNavigationDelegateTests.swift +++ b/Tests/NavigationTests/DistributedNavigationDelegateTests.swift @@ -25,8 +25,6 @@ import WebKit import XCTest @testable import Navigation -// swiftlint:disable unused_closure_parameter - @available(macOS 12.0, iOS 15.0, *) class DistributedNavigationDelegateTests: DistributedNavigationDelegateTestsBase { @@ -460,7 +458,7 @@ class DistributedNavigationDelegateTests: DistributedNavigationDelegateTestsBase ]) } - func testInstantlyOpenAboutPrefsUrlInNewWindow() throws { + func testInstantlyOpenAboutBlankUrlInNewWindow() throws { navigationDelegate.setResponders(.strong(NavigationResponderMock(defaultHandler: { _ in }))) navigationDelegateProxy.finishEventsDispatchTime = .instant @@ -485,7 +483,7 @@ class DistributedNavigationDelegateTests: DistributedNavigationDelegateTestsBase responder(at: 0).onDidFinish = { [unowned webView=withWebView(do: { $0 })] _ in counter += 1 if counter == 1 { - webView.evaluateJavaScript("window.open('about:prefs')") + webView.evaluateJavaScript("window.open('about:blank')") } else { eDidFinish.fulfill() } @@ -517,7 +515,7 @@ class DistributedNavigationDelegateTests: DistributedNavigationDelegateTestsBase .didCommit(Nav(action: navAct(1), .responseReceived, resp: resp(0), .committed)), .didFinish(Nav(action: navAct(1), .finished, resp: resp(0), .committed)), - .willStart(Nav(action: NavAction(req(urls.aboutPrefs, ["Referer": urls.local.separatedString]), .other, from: history[1], .userInitiated, src: main(urls.local), targ: main(webView: newWebView, secOrigin: urls.local.securityOrigin)), .approved, isCurrent: false)), + .willStart(Nav(action: NavAction(req(urls.aboutBlank, ["Referer": urls.local.separatedString]), .other, from: history[1], .userInitiated, src: main(urls.local), targ: main(webView: newWebView, secOrigin: urls.local.securityOrigin)), .approved, isCurrent: false)), .didStart(Nav(action: navAct(2), .started)), .didCommit(Nav(action: navAct(2), .started, .committed)), .didFinish(Nav(action: navAct(2), .finished, .committed)) @@ -717,70 +715,6 @@ class DistributedNavigationDelegateTests: DistributedNavigationDelegateTestsBase ]) } - func testOpenAboutPrefsInNewWindow() throws { - navigationDelegate.setResponders(.strong(NavigationResponderMock(defaultHandler: { _ in }))) - navigationDelegateProxy.finishEventsDispatchTime = .instant - - server.middleware = [{ [data] request in - return .ok(.html(data.html.string()!)) - }] - try server.start(8084) - - responder(at: 0).onDidFinish = { [unowned webView=withWebView(do: { $0 })] _ in - webView.evaluateJavaScript("window.open('about:prefs')") - } - var newFrameIdentity: FrameHandle! - responder(at: 0).onNavigationAction = { [urls, unowned webView=withWebView(do: { $0 })] navAction, _ in - if navAction.url.path == urls.local2.path { - XCTAssertTrue(navAction.isTargetingNewWindow) - newFrameIdentity = navAction.targetFrame?.handle - XCTAssertNotEqual(newFrameIdentity, webView.mainFrameHandle) - XCTAssertTrue(navAction.targetFrame?.isMainFrame == true) -#if _FRAME_HANDLE_ENABLED - XCTAssertNotEqual(newFrameIdentity.frameID, WKFrameInfo.defaultMainFrameHandle) -#endif - } - return .next - } - - let uiDelegate = WKUIDelegateMock() - var newWebViewConfig: WKWebViewConfiguration! - var newWebViewNavAction: WKNavigationAction! - let eCreateWebViewReceived = expectation(description: "createWebView received") - uiDelegate.createWebViewWithConfig = { config, navigationAction, _ in - newWebViewConfig = config - newWebViewNavAction = navigationAction - DispatchQueue.main.async { - eCreateWebViewReceived.fulfill() - } - return nil - } - - withWebView { webView in - webView.uiDelegate = uiDelegate - _=webView.load(req(urls.local)) - } - waitForExpectations(timeout: 5) - responder(at: 0).clear() - - let eDidFinish = expectation(description: "onDidFinish") - responder(at: 0).onDidFinish = { _ in - eDidFinish.fulfill() - } - - let newWebView = WKWebView(frame: .zero, configuration: newWebViewConfig) - newWebView.navigationDelegate = navigationDelegateProxy - newWebView.load(newWebViewNavAction.request) - waitForExpectations(timeout: 5) - - assertHistory(ofResponderAt: 0, equalsTo: [ - .willStart(Nav(action: NavAction(req(urls.aboutPrefs, ["Referer": urls.local.separatedString]), .other, from: history[1], src: main(webView: newWebView)), .approved, isCurrent: false)), - .didStart(Nav(action: navAct(2), .started)), - .didCommit(Nav(action: navAct(2), .started, .committed)), - .didFinish(Nav(action: navAct(2), .finished, .committed)), - ]) - } - func testLinkOpeningNewWindow() throws { navigationDelegate.setResponders(.strong(NavigationResponderMock(defaultHandler: { _ in }))) navigationDelegateProxy.finishEventsDispatchTime = .instant @@ -953,9 +887,18 @@ class DistributedNavigationDelegateTests: DistributedNavigationDelegateTestsBase waitForExpectations(timeout: 5) // #2 load URL#namedlink - eDidFinish = expectation(description: "#2") - customCallbacksHandler.didSameDocumentNavigation = { _, type in - if type == .sessionStatePop { eDidFinish.fulfill() } + eDidFinish = expectation(description: "Anchor") + let eStatePop = expectation(description: "State Pop") + customCallbacksHandler.didSameDocumentNavigation = { navigation, type in + switch type { + case .anchorNavigation: + eDidFinish.fulfill() + XCTAssertTrue(navigation.isCurrent) + case .sessionStatePop: + eStatePop.fulfill() + XCTAssertFalse(navigation.isCurrent) + default: XCTFail("Unexpected \(type.debugDescription)") + } } withWebView { webView in _=webView.load(req(urls.localHashed1)) @@ -963,6 +906,7 @@ class DistributedNavigationDelegateTests: DistributedNavigationDelegateTestsBase waitForExpectations(timeout: 5) responder(at: 0).clear() + eDidFinish = expectation(description: "didReload") let eNavAction = expectation(description: "onNavigationAction") responder(at: 0).onNavigationAction = { navigationAction, _ in @@ -975,12 +919,12 @@ class DistributedNavigationDelegateTests: DistributedNavigationDelegateTestsBase waitForExpectations(timeout: 5) assertHistory(ofResponderAt: 0, equalsTo: [ - .navigationAction(NavAction(req(urls.localHashed1, defaultHeaders.allowingExtraKeys, cachePolicy: .reloadIgnoringLocalCacheData), .reload, from: history[2], src: main(urls.localHashed1))), - .willStart(Nav(action: navAct(3), .approved, isCurrent: false)), - .didStart( Nav(action: navAct(3), .started)), - .response(Nav(action: navAct(3), .responseReceived, resp: .resp(urls.localHashed1, data.html.count, headers: .default + ["Content-Type": "text/html"]))), - .didCommit(Nav(action: navAct(3), .responseReceived, resp: resp(1), .committed)), - .didFinish(Nav(action: navAct(3), .finished, resp: resp(1), .committed)) + .navigationAction(NavAction(req(urls.localHashed1, defaultHeaders.allowingExtraKeys, cachePolicy: .reloadIgnoringLocalCacheData), .reload, from: history[3], src: main(urls.localHashed1))), + .willStart(Nav(action: navAct(4), .approved, isCurrent: false)), + .didStart(Nav(action: navAct(4), .started)), + .response(Nav(action: navAct(4), .responseReceived, resp: .resp(urls.localHashed1, data.html.count, headers: .default + ["Content-Type": "text/html"]))), + .didCommit(Nav(action: navAct(4), .responseReceived, resp: resp(1), .committed)), + .didFinish(Nav(action: navAct(4), .finished, resp: resp(1), .committed)) ]) } @@ -994,12 +938,12 @@ class DistributedNavigationDelegateTests: DistributedNavigationDelegateTestsBase responder(at: 0).onDidFinish = { _ in eDidFinish.fulfill() } withWebView { webView in - _=webView.load(req(urls.aboutPrefs)) + _=webView.load(req(urls.aboutBlank)) } waitForExpectations(timeout: 5) assertHistory(ofResponderAt: 0, equalsTo: [ - .willStart(Nav(action: .init(req(urls.aboutPrefs), .other, src: main()), .approved, isCurrent: false)), + .willStart(Nav(action: .init(req(urls.aboutBlank), .other, src: main()), .approved, isCurrent: false)), .didStart(Nav(action: navAct(1), .started)), .didCommit(Nav(action: navAct(1), .started, .committed)), .didFinish(Nav(action: navAct(1), .finished, .committed)) @@ -1728,7 +1672,4 @@ class DistributedNavigationDelegateTests: DistributedNavigationDelegateTestsBase } -// swiftlint:enable unused_closure_parameter -// swiftlint:enable opening_brace - #endif diff --git a/Tests/NavigationTests/Helpers/DistributedNavigationDelegateTestsHelpers.swift b/Tests/NavigationTests/Helpers/DistributedNavigationDelegateTestsHelpers.swift index ca142c7ef..6eef3cb0f 100644 --- a/Tests/NavigationTests/Helpers/DistributedNavigationDelegateTestsHelpers.swift +++ b/Tests/NavigationTests/Helpers/DistributedNavigationDelegateTestsHelpers.swift @@ -91,13 +91,14 @@ extension DistributedNavigationDelegateTestsBase { let webView = WKWebView(frame: .zero, configuration: configuration) webView.navigationDelegate = navigationDelegateProxy #if PRIVATE_NAVIGATION_DID_FINISH_CALLBACKS_ENABLED - currentHistoryItemIdentityCancellable = navigationDelegate.$currentHistoryItemIdentity.sink { [unowned self] historyItem in + currentHistoryItemIdentityCancellable = navigationDelegate.$currentHistoryItemIdentity.sink { [unowned self] (historyItem: HistoryItemIdentity?) in guard let historyItem, !self.history.contains(where: { $0.value == historyItem }), let lastNavigationAction = self.responder(at: 0)?.navigationActionsCache.max else { return } self.history[lastNavigationAction] = historyItem + // os_log("added history item #%d: %s", type: .debug, Int(lastNavigationAction), historyItem.debugDescription) } #endif return webView @@ -114,13 +115,17 @@ extension DistributedNavigationDelegateTestsBase { let local3 = #URL("http://localhost:8084/3") let local4 = #URL("http://localhost:8084/4") - let localHashed = #URL("http://localhost:8084#") - let localHashed1 = #URL("http://localhost:8084#navlink") - let localHashed2 = #URL("http://localhost:8084#navlink2") - let local3Hashed = #URL("http://localhost:8084/3#navlink") + let localHashed = URL(string: "http://localhost:8084#")! + let localHashed1 = URL(string: "http://localhost:8084#navlink")! + let localHashed2 = URL(string: "http://localhost:8084#navlink2")! + let localHashed3 = URL(string: "http://localhost:8084#navlink3")! + let local3Hashed = URL(string: "http://localhost:8084/3#navlink")! - let aboutBlank = #URL("about:blank") - let aboutPrefs = #URL("about:prefs") + let localTarget = URL(string: "http://localhost:8084/#target")! + let localTarget2 = URL(string: "http://localhost:8084/#target2")! + let localTarget3 = URL(string: "http://localhost:8084/#target3")! + + let aboutBlank = URL(string: "about:blank")! let post3 = #URL("http://localhost:8084/post3.html") } @@ -130,8 +135,10 @@ extension DistributedNavigationDelegateTestsBase { let html = """ - some data - + some data
+

+

+

""".data(using: .utf8)! @@ -199,6 +206,61 @@ extension DistributedNavigationDelegateTestsBase { """.data(using: .utf8)! + let sessionStatePushClientRedirectData: Data = """ + + + + """.data(using: .utf8)! + + let sameDocumentTestData: Data = """ + + + + + Same-document Navigation Test + + + +

Target Element


+

Target Element 2


+

Target Element 3


+ + + + + + """.data(using: .utf8)! + let customSchemeInteractionStateData = Data.sessionRestorationMagic + """ @@ -228,7 +290,7 @@ extension DistributedNavigationDelegateTestsBase { SessionHistoryEntryTitle SessionHistoryEntryURL - about:prefs + about:blank SessionHistoryVersion @@ -238,7 +300,7 @@ extension DistributedNavigationDelegateTestsBase { """.data(using: .utf8)! - let aboutPrefsAfterRegularNavigationInteractionStateData = Data.sessionRestorationMagic + """ + let aboutBlankAfterRegularNavigationInteractionStateData = Data.sessionRestorationMagic + """ @@ -356,8 +418,10 @@ extension DistributedNavigationDelegateTestsBase { return nil } - func navAct(_ idx: UInt64) -> NavAction { - return responder(at: 0).navigationActionsCache.dict[idx]! + func navAct(_ idx: UInt64, file: StaticString = #file, line: UInt = #line) -> NavAction { + return responder(at: 0).navigationActionsCache.dict[idx] ?? { + fatalError("No navigation action at index #\(idx): \(file):\(line)") + }() } func resp(_ idx: Int) -> NavResponse { return responder(at: 0).navigationResponses[idx] diff --git a/Tests/NavigationTests/Helpers/NavigationResponderMock.swift b/Tests/NavigationTests/Helpers/NavigationResponderMock.swift index b8c1a3781..e3e41a63b 100644 --- a/Tests/NavigationTests/Helpers/NavigationResponderMock.swift +++ b/Tests/NavigationTests/Helpers/NavigationResponderMock.swift @@ -57,6 +57,7 @@ enum TestsNavigationEvent: TestComparable { case navResponseWillBecomeDownload(Int, line: UInt = #line) case navResponseBecameDownload(Int, URL, line: UInt = #line) case didCommit(Nav, line: UInt = #line) + case didSameDocumentNavigation(Nav?, Int, line: UInt = #line) case didReceiveRedirect(NavAction, Nav, line: UInt = #line) case didFinish(Nav, line: UInt = #line) case didFail(Nav, /*code:*/ Int, line: UInt = #line) @@ -348,6 +349,17 @@ class NavigationResponderMock: NavigationResponder { onDidCommit?(navigation) ?? defaultHandler(event) } + var onSameDocumentNavigation: (@MainActor (Navigation?, WKSameDocumentNavigationType?) -> Void)? + func navigation(_ navigation: Navigation, didSameDocumentNavigationOf navigationType: WKSameDocumentNavigationType) { + if navigationActionsCache.dict[navigation.navigationAction.identifier] == nil { + navigationActionsCache.dict[navigation.navigationAction.identifier] = .init(navigation.navigationAction) + navigationActionsCache.max = max(navigationActionsCache.max, navigation.navigationAction.identifier) + } + + let event = append(.didSameDocumentNavigation(Nav(navigation), navigationType.rawValue)) + onSameDocumentNavigation?(navigation, navigationType) ?? defaultHandler(event) + } + var onDidFinish: (@MainActor (Navigation) -> Void)? func navigationDidFinish(_ navigation: Navigation) { let event = append(.didFinish(Nav(navigation))) diff --git a/Tests/NavigationTests/Helpers/NavigationTestHelpers.swift b/Tests/NavigationTests/Helpers/NavigationTestHelpers.swift index 0a155f02a..bae9234df 100644 --- a/Tests/NavigationTests/Helpers/NavigationTestHelpers.swift +++ b/Tests/NavigationTests/Helpers/NavigationTestHelpers.swift @@ -76,6 +76,8 @@ extension TestsNavigationEvent { return ".navResponseBecameDownload(\(arg), \(urlConst(for: arg2, in: context.urls)!))" case .didCommit(let arg, line: _): return ".didCommit(\(arg.encoded(context)))" + case .didSameDocumentNavigation(let arg, let arg2, line: _): + return ".didSameDocumentNavigation(\(arg?.encoded(context) ?? "nil"), \(arg2))" case .didReceiveRedirect(let navAct, let nav, line: _) where nav.navigationAction == navAct: return ".didReceiveRedirect(\(nav.encoded(context)))" case .didReceiveRedirect(let navAct, let nav, line: _): @@ -683,7 +685,7 @@ extension FrameInfo { func encoded(_ context: EncodingContext) -> String { let secOrigin = (securityOrigin == url.securityOrigin ? "" : "secOrigin: " + securityOrigin.encoded(context)) if self.isMainFrame { - return "main(" + (url.isEmpty ? "" : urlConst(for: url, in: context.urls)! + (secOrigin.isEmpty ? "" : ", ")) + secOrigin + ")" + return "main(" + (url.isEmpty ? "" : (urlConst(for: url, in: context.urls) ?? "!URL(\"\(url.absoluteString)\" not found in constants)") + (secOrigin.isEmpty ? "" : ", ")) + secOrigin + ")" } else { #if _FRAME_HANDLE_ENABLED let frameID = handle.frameID @@ -762,8 +764,13 @@ extension NavigationType { return ".redirect(.developer)" case .sessionRestoration: return ".restore" +#if PRIVATE_NAVIGATION_DID_FINISH_CALLBACKS_ENABLED + case .sameDocumentNavigation(let navigationType): + return ".sameDocumentNavigation(.\(navigationType.debugDescription))" +#else case .sameDocumentNavigation: return ".sameDocumentNavigation" +#endif case .other: return ".other" case .custom(let name): @@ -792,7 +799,7 @@ extension RedirectType { } extension HistoryItemIdentity { func encoded(_ context: EncodingContext) -> String { - let navigationActionIdx = context.history.first(where: { $0.value == self })!.key + let navigationActionIdx = context.history.keys.sorted().first(where: { context.history[$0]! == self })! return "history[\(navigationActionIdx)]" } } @@ -1068,8 +1075,8 @@ final class CustomCallbacksHandler: NSObject, NavigationResponder { self.didFailProvisionalLoadInFrame?(request, frame, error) } - var didSameDocumentNavigation: ((Navigation?, WKSameDocumentNavigationType?) -> Void)? - func navigation(_ navigation: Navigation?, didSameDocumentNavigationOf navigationType: WKSameDocumentNavigationType?) { + var didSameDocumentNavigation: (@MainActor (Navigation, WKSameDocumentNavigationType) -> Void)? + func navigation(_ navigation: Navigation, didSameDocumentNavigationOf navigationType: WKSameDocumentNavigationType) { self.didSameDocumentNavigation?(navigation, navigationType) } diff --git a/Tests/NavigationTests/NavigationAuthChallengeTests.swift b/Tests/NavigationTests/NavigationAuthChallengeTests.swift index 7a1455979..08228984a 100644 --- a/Tests/NavigationTests/NavigationAuthChallengeTests.swift +++ b/Tests/NavigationTests/NavigationAuthChallengeTests.swift @@ -25,10 +25,6 @@ import WebKit import XCTest @testable import Navigation -// swiftlint:disable unused_closure_parameter -// swiftlint:disable opening_brace -// swiftlint:disable trailing_comma - @available(macOS 12.0, iOS 15.0, *) class NavigationAuthChallengeTests: DistributedNavigationDelegateTestsBase { @@ -297,8 +293,4 @@ class NavigationAuthChallengeTests: DistributedNavigationDelegateTestsBase { } -// swiftlint:enable unused_closure_parameter -// swiftlint:enable opening_brace -// swiftlint:enable trailing_comma - #endif diff --git a/Tests/NavigationTests/NavigationBackForwardTests.swift b/Tests/NavigationTests/NavigationBackForwardTests.swift index e1283674a..98ea5578a 100644 --- a/Tests/NavigationTests/NavigationBackForwardTests.swift +++ b/Tests/NavigationTests/NavigationBackForwardTests.swift @@ -25,10 +25,6 @@ import WebKit import XCTest @testable import Navigation -// swiftlint:disable unused_closure_parameter -// swiftlint:disable trailing_comma -// swiftlint:disable opening_brace - @available(macOS 12.0, iOS 15.0, *) class NavigationBackForwardTests: DistributedNavigationDelegateTestsBase { @@ -389,216 +385,6 @@ class NavigationBackForwardTests: DistributedNavigationDelegateTestsBase { ]) } - func testGoBackWithSameDocumentNavigation() throws { - let customCallbacksHandler = CustomCallbacksHandler() - navigationDelegate.setResponders(.strong(NavigationResponderMock(defaultHandler: { _ in })), .weak(customCallbacksHandler)) - - server.middleware = [{ [data] request in - return .ok(.html(data.html.string()!)) - }] - try server.start(8084) - - var eDidFinish = expectation(description: "#1") - responder(at: 0).onDidFinish = { _ in eDidFinish.fulfill() } - responder(at: 0).onNavigationAction = { navigationAction, _ in .allow } - - // #1 load URL - withWebView { webView in - _=webView.load(req(urls.local)) - } - waitForExpectations(timeout: 5) - - // #2 load URL#namedlink - eDidFinish = expectation(description: "#2") - customCallbacksHandler.didSameDocumentNavigation = { _, type in - if type == .sessionStatePop { eDidFinish.fulfill() } - } - withWebView { webView in - _=webView.load(req(urls.localHashed1)) - } - waitForExpectations(timeout: 5) - - // #3 load URL#namedlink2 - eDidFinish = expectation(description: "#3") - withWebView { webView in - webView.evaluateJavaScript("window.location.href = '\(urls.localHashed2.string)'") - } - waitForExpectations(timeout: 5) - - // #4 load URL#namedlink - eDidFinish = expectation(description: "#4") - withWebView { webView in - webView.evaluateJavaScript("window.location.href = '\(urls.localHashed1.string)'") - } - waitForExpectations(timeout: 5) - - // #4.1 go back to URL#namedlink2 - eDidFinish = expectation(description: "#4.1") - withWebView { webView in - _=webView.goBack() - } - waitForExpectations(timeout: 5) - // #4.2 - eDidFinish = expectation(description: "#4.2") - withWebView { webView in - _=webView.goBack() - } - waitForExpectations(timeout: 5) - // #4.3 - eDidFinish = expectation(description: "#4.3") - withWebView { webView in - _=webView.goForward() - } - waitForExpectations(timeout: 5) - // #4.4 - eDidFinish = expectation(description: "#4.4") - withWebView { webView in - _=webView.goForward() - } - waitForExpectations(timeout: 5) - - // #5 load URL# - eDidFinish = expectation(description: "#5") - withWebView { webView in - webView.evaluateJavaScript("window.location.href = '\(urls.localHashed.string)'") - } - waitForExpectations(timeout: 5) - - // #6 load URL - eDidFinish = expectation(description: "#6") - withWebView { webView in - _=webView.load(req(urls.local)) - } - waitForExpectations(timeout: 5) - - // #7 go back to URL# - // !! here‘s the WebKit bug: no forward item will be present here - eDidFinish = expectation(description: "#7") - withWebView { webView in - _=webView.goBack() - } - waitForExpectations(timeout: 5) - - // #8 go back to URL#namedlink - eDidFinish = expectation(description: "#8") - withWebView { webView in - _=webView.goBack() - } - waitForExpectations(timeout: 5) - - assertHistory(ofResponderAt: 0, equalsTo: [ - // #1 load URL - .navigationAction(req(urls.local), .other, src: main()), - .willStart(Nav(action: navAct(1), .approved, isCurrent: false)), - .didStart(Nav(action: navAct(1), .started)), - .response(Nav(action: navAct(1), .responseReceived, resp: .resp(urls.local, data.html.count, headers: .default + ["Content-Type": "text/html"]))), - .didCommit(Nav(action: navAct(1), .responseReceived, resp: resp(0), .committed)), - .didFinish(Nav(action: navAct(1), .finished, resp: resp(0), .committed)), - - // #2 load URL#namedlink - .willStart(Nav(action: NavAction(req(urls.localHashed1), .sameDocumentNavigation, from: history[1], src: main(urls.local)), .approved, isCurrent: false)), - // #3 load URL#namedlink2 - .willStart(Nav(action: NavAction(req(urls.localHashed2, defaultHeaders + ["Referer": urls.local.separatedString]), .sameDocumentNavigation, from: history[2], src: main(urls.localHashed1)), .approved, isCurrent: false)), - // #3.1 load URL#namedlink - .willStart(Nav(action: NavAction(req(urls.localHashed1, defaultHeaders + ["Referer": urls.local.separatedString]), .sameDocumentNavigation, from: history[3], src: main(urls.localHashed2)), .approved, isCurrent: false)), - - // goBack/goForward ignored for same doc decidePolicyForNavigationAction not called - - // #5 load URL# - .willStart(Nav(action: NavAction(req(urls.localHashed, defaultHeaders + ["Referer": urls.local.separatedString]), .sameDocumentNavigation, from: history[4], src: main(urls.localHashed1)), .approved, isCurrent: false)), - - // #6 load URL - .navigationAction(req(urls.local), .other, from: history[5], src: main(urls.localHashed)), - .willStart(Nav(action: navAct(6), .approved, isCurrent: false)), - .didStart( Nav(action: navAct(6), .started)), - .response(Nav(action: navAct(6), .responseReceived, resp: resp(0))), - .didCommit(Nav(action: navAct(6), .responseReceived, resp: resp(0), .committed)), - .didFinish(Nav(action: navAct(6), .finished, resp: resp(0), .committed)), - - // history items replaced due to WebKit bug - // #7 go back to URL# - .willStart(Nav(action: NavAction(req(urls.localHashed, defaultHeaders.allowingExtraKeys), .backForw(-1), from: history[6], src: main(urls.local)), .approved, isCurrent: false)), - // #8 go back to URL#namedlink - .willStart(Nav(action: NavAction(req(urls.localHashed, defaultHeaders.allowingExtraKeys), .backForw(-1), from: history[7], src: main(urls.localHashed)), .approved, isCurrent: false)) - ]) - } - - func testJSHistoryManipulation() throws { - let customCallbacksHandler = CustomCallbacksHandler() - navigationDelegate.setResponders(.strong(NavigationResponderMock(defaultHandler: { _ in })), .weak(customCallbacksHandler)) - - server.middleware = [{ [data] request in - XCTAssertEqual(request.path, "/") - return .ok(.html(data.html.string()!)) - }] - try server.start(8084) - - var eDidFinish = expectation(description: "onDidFinish 1") - responder(at: 0).onDidFinish = { _ in eDidFinish.fulfill() } - - withWebView { webView in - _=webView.load(req(urls.local)) - } - waitForExpectations(timeout: 5) - responder(at: 0).clear() - - eDidFinish = expectation(description: "onDidFinish 2") - - var didPushStateCounter = 0 - let eDidSameDocumentNavigation = expectation(description: "onDidSameDocumentNavigation") - customCallbacksHandler.didSameDocumentNavigation = { _, type in - didPushStateCounter += 1 - if didPushStateCounter == 4 { - eDidSameDocumentNavigation.fulfill() - } - } - - withWebView { webView in - webView.evaluateJavaScript("history.pushState({page: 1}, '1', '/1')", in: nil, in: WKContentWorld.page) { _ in - webView.evaluateJavaScript("history.pushState({page: 3}, '3', '/3')", in: nil, in: WKContentWorld.page) { _ in - webView.evaluateJavaScript("history.pushState({page: 2}, '2', '/2')", in: nil, in: WKContentWorld.page) { _ in - webView.evaluateJavaScript("history.go(-1)", in: nil, in: WKContentWorld.page) { _ in - eDidFinish.fulfill() - } - } - } - } - } - waitForExpectations(timeout: 5) - - // now navigate from pseudo "/3" to "/3#hashed" - var eDidGoBack = expectation(description: "onDidGoToNamedLink") - customCallbacksHandler.didSameDocumentNavigation = { _, type in - if type == .sessionStatePop { eDidGoBack.fulfill() } - } - withWebView { webView in - _=webView.load(req(urls.local3Hashed)) - } - waitForExpectations(timeout: 5) - - // back - eDidGoBack = expectation(description: "onDidGoBack") - withWebView { webView in - _=webView.goBack() - } - waitForExpectations(timeout: 5) - - // back - eDidGoBack = expectation(description: "onDidGoBack 2") - withWebView { webView in - _=webView.goBack() - } - waitForExpectations(timeout: 5) - - assertHistory(ofResponderAt: 0, equalsTo: [ - .willStart(Nav(action: NavAction(req(urls.local3Hashed), .sameDocumentNavigation, from: history[1], src: main(urls.local3)), .approved, isCurrent: false)) - ]) - } - } -// swiftlint:enable unused_closure_parameter -// swiftlint:enable trailing_comma -// swiftlint:enable opening_brace - #endif diff --git a/Tests/NavigationTests/NavigationDownloadsTests.swift b/Tests/NavigationTests/NavigationDownloadsTests.swift index 5835b9df8..efa207d3e 100644 --- a/Tests/NavigationTests/NavigationDownloadsTests.swift +++ b/Tests/NavigationTests/NavigationDownloadsTests.swift @@ -25,10 +25,6 @@ import WebKit import XCTest @testable import Navigation -// swiftlint:disable unused_closure_parameter -// swiftlint:disable opening_brace -// swiftlint:disable trailing_comma - @available(macOS 12.0, iOS 15.0, *) class NavigationDownloadsTests: DistributedNavigationDelegateTestsBase { @@ -353,8 +349,4 @@ class NavigationDownloadsTests: DistributedNavigationDelegateTestsBase { } -// swiftlint:enable unused_closure_parameter -// swiftlint:enable opening_brace -// swiftlint:enable trailing_comma - #endif diff --git a/Tests/NavigationTests/NavigationRedirectsTests.swift b/Tests/NavigationTests/NavigationRedirectsTests.swift index 1431c94ff..5ea195f30 100644 --- a/Tests/NavigationTests/NavigationRedirectsTests.swift +++ b/Tests/NavigationTests/NavigationRedirectsTests.swift @@ -25,52 +25,9 @@ import WebKit import XCTest @testable import Navigation -// swiftlint:disable unused_closure_parameter -// swiftlint:disable trailing_comma -// swiftlint:disable opening_brace - @available(macOS 12.0, iOS 15.0, *) class NavigationRedirectsTests: DistributedNavigationDelegateTestsBase { - func testClientRedirectToSameDocument() throws { - let customCallbacksHandler = CustomCallbacksHandler() - navigationDelegate.setResponders(.strong(NavigationResponderMock(defaultHandler: { _ in })), .weak(customCallbacksHandler)) - - server.middleware = [{ [data] request in - return .ok(.html(data.sameDocumentClientRedirectData.string()!)) - }] - try server.start(8084) - - let eDidFinish = expectation(description: "#1") - responder(at: 0).onDidFinish = { _ in eDidFinish.fulfill() } - responder(at: 0).onNavigationAction = { navigationAction, _ in .allow } - - let eDidSameDocumentNavigation = expectation(description: "#2") - customCallbacksHandler.didSameDocumentNavigation = { _, type in - if type == .sessionStatePop { eDidSameDocumentNavigation.fulfill() } - } - - withWebView { webView in - _=webView.load(req(urls.local)) - } - waitForExpectations(timeout: 5) - - if case .didCommit = responder(at: 0).history[5] { - responder(at: 0).history.insert(responder(at: 0).history[5], at: 4) - } - assertHistory(ofResponderAt: 0, equalsTo: [ - .navigationAction(req(urls.local), .other, src: main()), - .willStart(Nav(action: navAct(1), .approved, isCurrent: false)), - .didStart(Nav(action: navAct(1), .started)), - .response(Nav(action: navAct(1), .responseReceived, resp: .resp(urls.local, data.sameDocumentClientRedirectData.count, headers: .default + ["Content-Type": "text/html"]))), - .didCommit(Nav(action: navAct(1), .responseReceived, resp: resp(0), .committed)), - - .willStart(Nav(action: NavAction(req(urls.localHashed1, defaultHeaders + ["Referer": urls.local.separatedString]), .sameDocumentNavigation, from: history[1], src: main(urls.local)), .approved, isCurrent: false)), - - .didFinish(Nav(action: navAct(1), .finished, resp: resp(0), .committed)) - ]) - } - func testClientRedirectFromHashedUrlToNonHashedUrl() throws { navigationDelegate.setResponders(.strong(NavigationResponderMock(defaultHandler: { _ in }))) @@ -1431,8 +1388,4 @@ class NavigationRedirectsTests: DistributedNavigationDelegateTestsBase { } -// swiftlint:enable unused_closure_parameter -// swiftlint:enable trailing_comma -// swiftlint:enable opening_brace - #endif diff --git a/Tests/NavigationTests/NavigationSessionRestorationTests.swift b/Tests/NavigationTests/NavigationSessionRestorationTests.swift index 3aa0a4a48..b8f709bc2 100644 --- a/Tests/NavigationTests/NavigationSessionRestorationTests.swift +++ b/Tests/NavigationTests/NavigationSessionRestorationTests.swift @@ -25,10 +25,6 @@ import WebKit import XCTest @testable import Navigation -// swiftlint:disable unused_closure_parameter -// swiftlint:disable trailing_comma -// swiftlint:disable opening_brace - @available(macOS 12.0, iOS 15.0, *) class NavigationSessionRestorationTests: DistributedNavigationDelegateTestsBase { @@ -65,7 +61,7 @@ class NavigationSessionRestorationTests: DistributedNavigationDelegateTestsBase waitForExpectations(timeout: 5) assertHistory(ofResponderAt: 0, equalsTo: [ - .willStart(Nav(action: .init(req(urls.aboutPrefs, [:], cachePolicy: .returnCacheDataElseLoad), .restore, src: main()), .approved, isCurrent: false)), + .willStart(Nav(action: .init(req(urls.aboutBlank, [:], cachePolicy: .returnCacheDataElseLoad), .restore, src: main()), .approved, isCurrent: false)), .didStart(Nav(action: navAct(1), .started)), .didCommit(Nav(action: navAct(1), .started, .committed)), .didFinish(Nav(action: navAct(1), .finished, .committed)), @@ -254,14 +250,14 @@ class NavigationSessionRestorationTests: DistributedNavigationDelegateTestsBase ]) } - func testWhenAboutPrefsSessionIsRestored_navigationTypeIsSessionRestoration() { + func testWhenAboutBlankSessionIsRestored_navigationTypeIsSessionRestoration() { navigationDelegate.setResponders(.strong(NavigationResponderMock(defaultHandler: { _ in }))) let eDidFinish = expectation(description: "onDidFinish") responder(at: 0).onDidFinish = { _ in eDidFinish.fulfill() } withWebView { webView in - webView.interactionState = data.aboutPrefsAfterRegularNavigationInteractionStateData + webView.interactionState = data.aboutBlankAfterRegularNavigationInteractionStateData } waitForExpectations(timeout: 5) @@ -275,8 +271,4 @@ class NavigationSessionRestorationTests: DistributedNavigationDelegateTestsBase } -// swiftlint:enable unused_closure_parameter -// swiftlint:enable trailing_comma -// swiftlint:enable opening_brace - #endif diff --git a/Tests/NavigationTests/SameDocumentNavigationTests.swift b/Tests/NavigationTests/SameDocumentNavigationTests.swift new file mode 100644 index 000000000..4d65002e2 --- /dev/null +++ b/Tests/NavigationTests/SameDocumentNavigationTests.swift @@ -0,0 +1,453 @@ +// +// SameDocumentNavigationTests.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. +// + +#if PRIVATE_NAVIGATION_DID_FINISH_CALLBACKS_ENABLED + +import Combine +import Common +import Swifter +import WebKit +import XCTest +@testable import Navigation + +@available(macOS 12.0, iOS 15.0, *) +class SameDocumentNavigationTests: DistributedNavigationDelegateTestsBase { + + override func setUp() { + super.setUp() + + setenv("OS_LOG_CAPTURE", "1", 1) + setenv("OS_LOG_DEBUG", "1", 1) + } + + func testGoBackWithSameDocumentNavigation() throws { + os_log(.info, log: .default, "THIS IS TEST MESSAGE") + print("this is printed message") + + navigationDelegate.setResponders(.strong(NavigationResponderMock(defaultHandler: { _ in }))) + + server.middleware = [{ [data] request in + return .ok(.html(data.html.string()!)) + }] + try server.start(8084) + + var eDidFinish = expectation(description: "#1") + responder(at: 0).onDidFinish = { _ in eDidFinish.fulfill() } + responder(at: 0).onNavigationAction = { navigationAction, _ in .allow } + + print("#1 load URL") + withWebView { webView in + _=webView.load(req(urls.local)) + } + waitForExpectations(timeout: 5) + + print("#2 load URL#namedlink1") + eDidFinish = expectation(description: "#2") + responder(at: 0).onSameDocumentNavigation = { _, type in + if type == .sessionStatePop { eDidFinish.fulfill() } + } + withWebView { webView in + _=webView.load(req(urls.localHashed1)) + } + waitForExpectations(timeout: 5) + + print("#3 load URL#namedlink2") + eDidFinish = expectation(description: "#3") + withWebView { webView in + webView.evaluateJavaScript("window.location.href = '\(urls.localHashed2.string)'") + } + waitForExpectations(timeout: 5) + + print("#4 load URL#namedlink3") + eDidFinish = expectation(description: "#4") + withWebView { webView in + webView.evaluateJavaScript("window.location.href = '\(urls.localHashed3.string)'") + } + waitForExpectations(timeout: 5) + + print("#4.1 go back to URL#namedlink2") + eDidFinish = expectation(description: "#4.1") + withWebView { webView in + _=webView.goBack() + } + waitForExpectations(timeout: 5) + print("#4.2 go back to URL#namedlink1") + eDidFinish = expectation(description: "#4.2") + withWebView { webView in + _=webView.goBack() + } + waitForExpectations(timeout: 5) + print("#4.3 go forward to URL#namedlink2") + eDidFinish = expectation(description: "#4.3") + withWebView { webView in + _=webView.goForward() + } + waitForExpectations(timeout: 5) + print("#4.4 go forward to URL#namedlink3") + eDidFinish = expectation(description: "#4.4") + withWebView { webView in + _=webView.goForward() + } + waitForExpectations(timeout: 5) + + print("#5 load URL#") + eDidFinish = expectation(description: "#5") + withWebView { webView in + webView.evaluateJavaScript("window.location.href = '\(urls.localHashed.string)'") + } + waitForExpectations(timeout: 5) + + print("#6 load URL") + eDidFinish = expectation(description: "#6") + withWebView { webView in + _=webView.load(req(urls.local)) + } + waitForExpectations(timeout: 5) + + print("#7 go back to URL#") + // !! here‘s the WebKit bug: no forward item will be present here + eDidFinish = expectation(description: "#7") + withWebView { webView in + _=webView.goBack() + } + waitForExpectations(timeout: 5) + + print("#8 go back to URL#namedlink") + eDidFinish = expectation(description: "#8") + withWebView { webView in + _=webView.goBack() + } + waitForExpectations(timeout: 5) + + assertHistory(ofResponderAt: 0, equalsTo: [ + // #1 load URL + .navigationAction(/*#1*/req(urls.local), .other, src: main()), + .willStart(Nav(action: navAct(1), .approved, isCurrent: false)), + .didStart(Nav(action: navAct(1), .started)), + .response(Nav(action: navAct(1), .responseReceived, resp: .resp(urls.local, data.html.count, headers: .default + ["Content-Type": "text/html"]))), + .didCommit(Nav(action: navAct(1), .responseReceived, resp: resp(0), .committed)), + .didFinish(Nav(action: navAct(1), .finished, resp: resp(0), .committed)), + + // #2 load URL#namedlink1 + .willStart(Nav(action: NavAction(/*#2*/req(urls.localHashed1), .sameDocumentNavigation(.anchorNavigation), from: history[1], src: main(urls.local)), .approved, isCurrent: false)), + .didSameDocumentNavigation(Nav(action: NavAction(/*#3*/req(urls.localHashed1, [:]), .sameDocumentNavigation(.sessionStatePop), from: history[1], src: main(urls.localHashed1)), .finished, isCurrent: false), 3), + .didSameDocumentNavigation(Nav(action: navAct(2), .finished), 0), + + // #3 load URL#namedlink2 + .willStart(Nav(action: NavAction(/*#4*/req(urls.localHashed2, defaultHeaders + ["Referer": urls.local.separatedString]), .sameDocumentNavigation(.anchorNavigation), from: history[3], .userInitiated, src: main(urls.localHashed1)), .approved, isCurrent: false)), + .didSameDocumentNavigation(Nav(action: NavAction(/*#5*/req(urls.localHashed2, [:]), .sameDocumentNavigation(.sessionStatePop), from: history[3], .userInitiated, src: main(urls.localHashed2)), .finished, isCurrent: false), 3), + .didSameDocumentNavigation(Nav(action: navAct(4), .finished), 0), + + // #4 load URL#namedlink3 + .willStart(Nav(action: NavAction(/*#6*/req(urls.localHashed3, defaultHeaders + ["Referer": urls.local.separatedString]), .sameDocumentNavigation(.anchorNavigation), from: history[5], src: main(urls.localHashed2)), .approved, isCurrent: false)), + .didSameDocumentNavigation(Nav(action: NavAction(/*#7*/req(urls.localHashed3, [:]), .sameDocumentNavigation(.sessionStatePop), from: history[5], .userInitiated, src: main(urls.localHashed3)), .finished, isCurrent: false), 3), + .didSameDocumentNavigation(Nav(action: navAct(6), .finished), 0), + + // #4.1 go back to URL#namedlink2 + .didSameDocumentNavigation(Nav(action: NavAction(/*#8*/req(urls.localHashed2, [:]), .sameDocumentNavigation(.sessionStatePop), from: history[7], src: main(urls.localHashed2)), .finished), 3), + .didSameDocumentNavigation(Nav(action: navAct(6), .finished, isCurrent: false), 0), + + // #4.2 go back to URL#namedlink1 + .didSameDocumentNavigation(Nav(action: NavAction(/*#9*/req(urls.localHashed1, [:]), .sameDocumentNavigation(.sessionStatePop), from: history[5], src: main(urls.localHashed1)), .finished), 3), + .didSameDocumentNavigation(Nav(action: navAct(6), .finished, isCurrent: false), 0), + + // #4.3 go forward to URL#namedlink2 + .didSameDocumentNavigation(Nav(action: NavAction(/*#10*/req(urls.localHashed2, [:]), .sameDocumentNavigation(.sessionStatePop), from: history[3], src: main(urls.localHashed2)), .finished), 3), + .didSameDocumentNavigation(Nav(action: navAct(6), .finished, isCurrent: false), 0), + + // #4.3 go forward to URL#namedlink3 + .didSameDocumentNavigation(Nav(action: NavAction(/*#11*/req(urls.localHashed3, [:]), .sameDocumentNavigation(.sessionStatePop), from: history[5], src: main(urls.localHashed3)), .finished), 3), + .didSameDocumentNavigation(Nav(action: navAct(6), .finished, isCurrent: false), 0), + + // goBack/goForward ignored for same doc decidePolicyForNavigationAction not called + + // #5 load URL# + .willStart(Nav(action: NavAction(/*#12*/req(urls.localHashed, defaultHeaders + ["Referer": urls.local.separatedString]), .sameDocumentNavigation(.anchorNavigation), from: history[7], .userInitiated, src: main(urls.localHashed3)), .approved, isCurrent: false)), + .didSameDocumentNavigation(Nav(action: NavAction(/*#13*/req(urls.localHashed, [:]), .sameDocumentNavigation(.sessionStatePop), from: history[7], .userInitiated, src: main(urls.localHashed)), .finished, isCurrent: false), 3), + .didSameDocumentNavigation(Nav(action: navAct(12), .finished), 0), + + // #6 load URL + .navigationAction(NavAction(/*#14*/req(urls.local), .other, from: history[13], src: main(urls.localHashed))), + .willStart(Nav(action: navAct(14), .approved, isCurrent: false)), + .didStart(Nav(action: navAct(14), .started)), + .response(Nav(action: navAct(14), .responseReceived, resp: resp(0))), + .didCommit(Nav(action: navAct(14), .responseReceived, resp: resp(0), .committed)), + .didFinish(Nav(action: navAct(14), .finished, resp: resp(0), .committed)), + + // #7 go back to URL# + .willStart(Nav(action: NavAction(/*#15*/req(urls.localHashed, defaultHeaders.allowingExtraKeys), .backForw(-1), from: history[14], src: main(urls.local)), .approved, isCurrent: false)), + .didSameDocumentNavigation(Nav(action: NavAction(/*#16*/req(urls.localHashed, [:]), .sameDocumentNavigation(.sessionStatePop), from: history[14], src: main(urls.localHashed)), .finished), 3), + .didSameDocumentNavigation(Nav(action: navAct(15), .approved, isCurrent: false), 0), + + // #8 go back to URL#namedlink + .willStart(Nav(action: NavAction(req(urls.localHashed, defaultHeaders.allowingExtraKeys), .backForw(-1), from: history[16], src: main(urls.localHashed)), .approved, isCurrent: false)), + .didSameDocumentNavigation(Nav(action: NavAction(req(urls.localHashed, [:]), .sameDocumentNavigation(.sessionStatePop), from: history[16], src: main(urls.localHashed)), .finished), 3) + ]) + } + + func testJSHistoryManipulation() throws { + let customCallbacksHandler = CustomCallbacksHandler() + navigationDelegate.setResponders(.strong(NavigationResponderMock(defaultHandler: { _ in })), .weak(customCallbacksHandler)) + + server.middleware = [{ [data] request in + XCTAssertEqual(request.path, "/") + return .ok(.html(data.html.string()!)) + }] + try server.start(8084) + + var eDidFinish = expectation(description: "onDidFinish 1") + responder(at: 0).onDidFinish = { _ in eDidFinish.fulfill() } + + withWebView { webView in + _=webView.load(req(urls.local)) + } + waitForExpectations(timeout: 5) + responder(at: 0).clear() + + eDidFinish = expectation(description: "onDidFinish 2") + + var didPushStateCounter = 0 + let eDidSameDocumentNavigation = expectation(description: "onDidSameDocumentNavigation") + customCallbacksHandler.didSameDocumentNavigation = { _, type in + didPushStateCounter += 1 + if didPushStateCounter == 4 { + eDidSameDocumentNavigation.fulfill() + } + } + + print("#1 push /1, /3, /2, go back") + withWebView { webView in + webView.evaluateJavaScript("history.pushState({page: 1}, '1', '/1')", in: nil, in: WKContentWorld.page) { _ in + webView.evaluateJavaScript("history.pushState({page: 3}, '3', '/3')", in: nil, in: WKContentWorld.page) { _ in + webView.evaluateJavaScript("history.pushState({page: 2}, '2', '/2')", in: nil, in: WKContentWorld.page) { _ in + webView.evaluateJavaScript("history.go(-1)", in: nil, in: WKContentWorld.page) { _ in + eDidFinish.fulfill() + } + } + } + } + } + waitForExpectations(timeout: 5) + + print("#2 navigate from pseudo `/3` to `/3#hashed`") + var eDidGoBack = expectation(description: "onDidGoToNamedLink") + customCallbacksHandler.didSameDocumentNavigation = { _, type in + if type == .sessionStatePop { eDidGoBack.fulfill() } + } + withWebView { webView in + _=webView.load(req(urls.local3Hashed)) + } + waitForExpectations(timeout: 5) + + print("#3 go back") + eDidGoBack = expectation(description: "onDidGoBack") + withWebView { webView in + _=webView.goBack() + } + waitForExpectations(timeout: 5) + + print("#4 go back") + eDidGoBack = expectation(description: "onDidGoBack 2") + withWebView { webView in + _=webView.goBack() + } + waitForExpectations(timeout: 5) + + assertHistory(ofResponderAt: 0, equalsTo: [ + // #1 + // push /1 + .didSameDocumentNavigation(Nav(action: NavAction(/*#2*/req(urls.local1, [:]), .sameDocumentNavigation(.sessionStatePush), from: history[1], .userInitiated, src: main(urls.local1)), .finished), 1), + // push /3 + .didSameDocumentNavigation(Nav(action: NavAction(/*#3*/req(urls.local3, [:]), .sameDocumentNavigation(.sessionStatePush), from: history[2], .userInitiated, src: main(urls.local3)), .finished), 1), + // push /2 + .didSameDocumentNavigation(Nav(action: NavAction(/*#4*/req(urls.local2, [:]), .sameDocumentNavigation(.sessionStatePush), from: history[3], .userInitiated, src: main(urls.local2)), .finished), 1), + // go back to /3 + .didSameDocumentNavigation(Nav(action: NavAction(/*#5*/req(urls.local3, [:]), .sameDocumentNavigation(.sessionStatePop), from: history[4], .userInitiated, src: main(urls.local3)), .finished), 3), + + // #2 navigate from pseudo `/3` to `/3#hashed` + .willStart(Nav(action: NavAction(/*#6*/req(urls.local3Hashed), .sameDocumentNavigation(.anchorNavigation), from: history[3], src: main(urls.local3)), .approved, isCurrent: false)), + .didSameDocumentNavigation(Nav(action: NavAction(/*#7*/req(urls.local3Hashed, [:]), .sameDocumentNavigation(.sessionStatePop), from: history[3], src: main(urls.local3Hashed)), .finished, isCurrent: false), 3), + .didSameDocumentNavigation(Nav(action: navAct(6), .finished), 0), + + // #3 go back + .didSameDocumentNavigation(Nav(action: NavAction(/*#8*/req(urls.local3, [:]), .sameDocumentNavigation(.sessionStatePop), from: history[7], src: main(urls.local3)), .finished), 3), + .didSameDocumentNavigation(Nav(action: navAct(6), .finished, isCurrent: false), 0), + + // #4 go back + .didSameDocumentNavigation(Nav(action: NavAction(/*#9*/req(urls.local1, [:]), .sameDocumentNavigation(.sessionStatePop), from: history[3], src: main(urls.local1)), .finished), 3) + ]) + } + + func testSameDocumentNavigations() throws { + let customCallbacksHandler = CustomCallbacksHandler() + navigationDelegate.setResponders(.strong(NavigationResponderMock(defaultHandler: { _ in })), .weak(customCallbacksHandler)) + + server.middleware = [{ [data] request in + XCTAssertEqual(request.path, "/") + return .ok(.html(data.sameDocumentTestData.string()!)) + }] + try server.start(8084) + + // 1. Load the Initial Page + var eDidFinish = expectation(description: "onDidFinish 1") + responder(at: 0).onDidFinish = { _ in eDidFinish.fulfill() } + + withWebView { webView in + _=webView.load(req(urls.local)) + } + waitForExpectations(timeout: 5) + responder(at: 0).clear() + + // 2. Anchor Navigation (#target) + var expectedNavigationTypes = [WKSameDocumentNavigationType]() + customCallbacksHandler.didSameDocumentNavigation = { _, type in + let idx = expectedNavigationTypes.firstIndex(of: type) + XCTAssertNotNil(idx, type.debugDescription) + _=idx.map { expectedNavigationTypes.remove(at: $0) } + if expectedNavigationTypes.isEmpty { + eDidFinish.fulfill() + } + } + eDidFinish = expectation(description: "Anchor navigation") + expectedNavigationTypes = [.sessionStatePop, .anchorNavigation] + withWebView { webView in + webView.evaluateJavaScript("performNavigation('anchorNavigation')") + } + waitForExpectations(timeout: 5) + + // 2. Session State Push (#target2) + eDidFinish = expectation(description: "Session State Push") + expectedNavigationTypes = [.sessionStatePush] + withWebView { webView in + webView.evaluateJavaScript("performNavigation('sessionStatePush')") + } + waitForExpectations(timeout: 5) + + // 3. Session State Replace (#target3) + eDidFinish = expectation(description: "Session State Replace") + expectedNavigationTypes = [.sessionStateReplace] + withWebView { webView in + webView.evaluateJavaScript("performNavigation('sessionStateReplace')") + } + waitForExpectations(timeout: 5) + + // 4. Session State Pop (#target) + eDidFinish = expectation(description: "Session State Pop") + expectedNavigationTypes = [.sessionStatePop, .anchorNavigation] + withWebView { webView in + webView.evaluateJavaScript("performNavigation('sessionStatePop')") + } + waitForExpectations(timeout: 5) + + assertHistory(ofResponderAt: 0, equalsTo: [ + // 2. Anchor Navigation (#target) + .willStart(Nav(action: /*#2*/NavAction(req(urls.localTarget, defaultHeaders + ["Referer": urls.local.separatedString]), .sameDocumentNavigation(.anchorNavigation), from: history[1], .userInitiated, src: main(urls.local)), .approved, isCurrent: false)), + .didSameDocumentNavigation(Nav(action: /*#3*/NavAction(req(urls.localTarget, [:]), .sameDocumentNavigation(.sessionStatePop), from: history[1], .userInitiated, src: main(urls.localTarget)), .finished, isCurrent: false), 3), + .didSameDocumentNavigation(Nav(action: navAct(2), .finished), 0), + + // 2. Session State Push (#target2) + .didSameDocumentNavigation(Nav(action: /*#4*/NavAction(req(urls.localTarget2, [:]), .sameDocumentNavigation(.sessionStatePush), from: history[3], .userInitiated, src: main(urls.localTarget2)), .finished), 1), + + // 3. Session State Replace (#target3) + .didSameDocumentNavigation(Nav(action: /*#5*/NavAction(req(urls.localTarget3, [:]), .sameDocumentNavigation(.sessionStateReplace), from: history[4], .userInitiated, src: main(urls.localTarget3)), .finished), 2), + + // 4. Session State Pop (#target) + .didSameDocumentNavigation(Nav(action: /*#6*/NavAction(req(urls.localTarget, [:]), .sameDocumentNavigation(.sessionStatePop), from: history[4], .userInitiated, src: main(urls.localTarget)), .finished), 3), + .didSameDocumentNavigation(Nav(action: navAct(2), .finished, isCurrent: false), 0) + ]) + } + + func testClientRedirectToSameDocument() throws { + let customCallbacksHandler = CustomCallbacksHandler() + navigationDelegate.setResponders(.strong(NavigationResponderMock(defaultHandler: { _ in })), .weak(customCallbacksHandler)) + + server.middleware = [{ [data] request in + return .ok(.html(data.sameDocumentClientRedirectData.string()!)) + }] + try server.start(8084) + + let eDidFinish = expectation(description: "#1") + responder(at: 0).onDidFinish = { _ in eDidFinish.fulfill() } + responder(at: 0).onNavigationAction = { navigationAction, _ in .allow } + + let eDidSameDocumentNavigation = expectation(description: "#2") + customCallbacksHandler.didSameDocumentNavigation = { _, type in + if type == .sessionStatePop { eDidSameDocumentNavigation.fulfill() } + } + + withWebView { webView in + _=webView.load(req(urls.local)) + } + waitForExpectations(timeout: 5) + + if case .didCommit = responder(at: 0).history[5] { + responder(at: 0).history.insert(responder(at: 0).history[5], at: 4) + } + assertHistory(ofResponderAt: 0, equalsTo: [ + .navigationAction(req(urls.local), .other, src: main()), + .willStart(Nav(action: navAct(1), .approved, isCurrent: false)), + .didStart(Nav(action: navAct(1), .started)), + .response(Nav(action: navAct(1), .responseReceived, resp: .resp(urls.local, data.sameDocumentClientRedirectData.count, headers: .default + ["Content-Type": "text/html"]))), + .didCommit(Nav(action: navAct(1), .responseReceived, resp: resp(0), .committed)), + + .didReceiveRedirect(NavAction(req(urls.localHashed1, defaultHeaders + ["Referer": urls.local.separatedString]), .sameDocumentNavigation(.anchorNavigation), from: history[1], src: main(urls.local)), Nav(action: navAct(1), .redirected(.client), resp: resp(0), .committed)), + .willStart(Nav(action: NavAction(req(urls.localHashed1, defaultHeaders + ["Referer": urls.local.separatedString]), .sameDocumentNavigation(.anchorNavigation), from: history[1], src: main(urls.local)), .approved, isCurrent: false)), + + .didSameDocumentNavigation(Nav(action: NavAction(req(urls.localHashed1, [:]), .sameDocumentNavigation(.sessionStatePop), from: history[1], src: main(urls.localHashed1)), .finished, isCurrent: false), 3), + .didSameDocumentNavigation(Nav(action: navAct(2), .finished), 0), + .didFinish(Nav(action: navAct(1), .finished, resp: resp(0), .committed, isCurrent: false)), + ]) + } + + func testClientRedirectUsingSessionStatePush() throws { + let customCallbacksHandler = CustomCallbacksHandler() + navigationDelegate.setResponders(.strong(NavigationResponderMock(defaultHandler: { _ in })), .weak(customCallbacksHandler)) + + server.middleware = [{ [data] request in + return .ok(.html(data.sessionStatePushClientRedirectData.string()!)) + }] + try server.start(8084) + + let eDidFinish = expectation(description: "#1") + responder(at: 0).onDidFinish = { _ in eDidFinish.fulfill() } + responder(at: 0).onNavigationAction = { navigationAction, _ in .allow } + + withWebView { webView in + _=webView.load(req(urls.local)) + } + waitForExpectations(timeout: 5) + + if case .didCommit = responder(at: 0).history[5] { + responder(at: 0).history.insert(responder(at: 0).history[5], at: 4) + } + assertHistory(ofResponderAt: 0, equalsTo: [ + .navigationAction(req(urls.local), .other, src: main()), + .willStart(Nav(action: navAct(1), .approved, isCurrent: false)), + .didStart(Nav(action: navAct(1), .started)), + .response(Nav(action: navAct(1), .responseReceived, resp: .resp(urls.local, data.sessionStatePushClientRedirectData.count, headers: .default + ["Content-Type": "text/html"]))), + .didCommit(Nav(action: navAct(1), .responseReceived, resp: resp(0), .committed)), + + .didSameDocumentNavigation(Nav(action: NavAction(req(urls.localHashed1, [:]), .sameDocumentNavigation(.sessionStatePush), from: history[1], src: main(urls.localHashed1)), .finished), 1), + + .didFinish(Nav(action: navAct(1), .finished, resp: resp(0), .committed, isCurrent: false)), + ]) + } + +} + +#endif