Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions Source/NavigationHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,21 @@ import Foundation
///
/// - Note: See `NumbersViewController` in the demo app for an example.
public protocol NavigationHandler: AnyObject {
func route(_ url: URL)
/// Routes to the given URL.
/// - Parameters:
/// - url: the URL to visit.
/// - options: passed options will override default `advance` visit options.
/// - parameters: provide context relevant to `url`.
func route(_ url: URL, options: VisitOptions?, parameters: [String: Any]?)

func route(_ proposal: VisitProposal)
}

extension Navigator: NavigationHandler {
public func route(_ url: URL) {
public extension NavigationHandler {
/// Routes to the given URL using the default `advance` visit options.
func route(_ url: URL) {
route(url, options: VisitOptions(action: .advance), parameters: nil)
}
}

extension Navigator: NavigationHandler {}
38 changes: 38 additions & 0 deletions Source/Turbo/Navigator/Navigating.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import Foundation
import UIKit

/// A testable abstraction over `Navigator`.
///
/// Refines `NavigationHandler` (which provides the `route` overloads) with the
/// navigation operations and view controller accessors that
/// `RouteDecisionHandler` and `WebViewPolicyDecisionHandler` implementations rely on.
public protocol Navigating: NavigationHandler {
/// Pops the top controller on the presented navigation stack.
func pop(animated: Bool)

/// Dismisses a modally presented controller if present, then pops the entire navigation stack.
func clearAll(animated: Bool)

/// Reloads the main and modal `Session`.
func reload()

/// The root navigation controller of the main navigation stack.
var rootViewController: UINavigationController { get }

/// The root navigation controller of the modal navigation stack.
var modalRootViewController: UINavigationController { get }

/// The navigation controller currently presented to the user.
var activeNavigationController: UINavigationController { get }

/// The URL currently displayed by the top visitable on the active navigation stack, if any.
var currentVisitableURL: URL? { get }
}

public extension Navigating {
var currentVisitableURL: URL? {
(activeNavigationController.topViewController as? VisitableViewController)?.currentVisitableURL
}
}

extension Navigator: Navigating {}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public final class AppNavigationRouteDecisionHandler: RouteDecisionHandler {

public func handle(location: URL,
configuration: Navigator.Configuration,
navigator: Navigator) -> Router.Decision {
navigator: Navigating) -> Router.Decision {
logger.info("Routing \(location.absoluteString)")
return .navigate
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public final class SafariViewControllerRouteDecisionHandler: RouteDecisionHandle

public func handle(location: URL,
configuration: Navigator.Configuration,
navigator: Navigator) -> HotwireNative.Router.Decision {
navigator: Navigating) -> HotwireNative.Router.Decision {
Task { @MainActor in
await open(externalURL: location,
viewController: navigator.activeNavigationController)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public final class SystemNavigationRouteDecisionHandler: RouteDecisionHandler {

public func handle(location: URL,
configuration: Navigator.Configuration,
navigator: Navigator) -> Router.Decision {
navigator: Navigating) -> Router.Decision {
UIApplication.shared.open(location)

return .cancel
Expand Down
2 changes: 1 addition & 1 deletion Source/Turbo/Navigator/Routing/RouteDecisionHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,5 @@ public protocol RouteDecisionHandler {
/// - navigator: The navigator instance responsible for the navigation.
func handle(location: URL,
configuration: Navigator.Configuration,
navigator: Navigator) -> Router.Decision
navigator: Navigating) -> Router.Decision
}
2 changes: 1 addition & 1 deletion Source/Turbo/Navigator/Routing/Router.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public final class Router {

func decideRoute(for location: URL,
configuration: Navigator.Configuration,
navigator: Navigator) -> Router.Decision {
navigator: Navigating) -> Router.Decision {
for handler in decisionHandlers {
if handler.matches(location: location, configuration: configuration) {
logger.debug("[Router] handler match found handler: \(handler.name) location: \(location)")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public struct ExternalNavigationWebViewPolicyDecisionHandler: WebViewPolicyDecis

public func handle(navigationAction: WKNavigationAction,
configuration: Navigator.Configuration,
navigator: Navigator) -> WebViewPolicyManager.Decision {
navigator: Navigating) -> WebViewPolicyManager.Decision {
if let scheme = navigationAction.request.url?.scheme,
invalidSchemes.contains(scheme) {
return .cancel
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public struct LinkActivatedWebViewPolicyDecisionHandler: WebViewPolicyDecisionHa

public func handle(navigationAction: WKNavigationAction,
configuration: Navigator.Configuration,
navigator: Navigator) -> WebViewPolicyManager.Decision {
navigator: Navigating) -> WebViewPolicyManager.Decision {
return .cancel
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public struct NewWindowWebViewPolicyDecisionHandler: WebViewPolicyDecisionHandle

public func handle(navigationAction: WKNavigationAction,
configuration: Navigator.Configuration,
navigator: Navigator) -> WebViewPolicyManager.Decision {
navigator: Navigating) -> WebViewPolicyManager.Decision {
if let url = navigationAction.request.url {
navigator.route(url)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public struct ReloadWebViewPolicyDecisionHandler: WebViewPolicyDecisionHandler {

public func handle(navigationAction: WKNavigationAction,
configuration: Navigator.Configuration,
navigator: Navigator) -> WebViewPolicyManager.Decision {
navigator: Navigating) -> WebViewPolicyManager.Decision {
logger.info("ReloadWebViewPolicyDecisionHandler: Reloading at \(navigationAction.request.url?.absoluteString ?? "<web view has no url>")")
navigator.reload()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public final class WebViewPolicyManager {

func decidePolicy(for navigationAction: WKNavigationAction,
configuration: Navigator.Configuration,
navigator: Navigator) -> WebViewPolicyManager.Decision {
navigator: Navigating) -> WebViewPolicyManager.Decision {
for handler in policyDecisionHandlers {
if handler.matches(navigationAction: navigationAction, configuration: configuration) {
logger.debug("[WebViewPolicyManager] handler match found handler: \(handler.name) navigation action:\(navigationAction)")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,5 @@ public protocol WebViewPolicyDecisionHandler {
/// whether to allow or cancel the WebView navigation.
func handle(navigationAction: WKNavigationAction,
configuration: Navigator.Configuration,
navigator: Navigator) -> WebViewPolicyManager.Decision
navigator: Navigating) -> WebViewPolicyManager.Decision
}
4 changes: 2 additions & 2 deletions Source/Turbo/ViewControllers/HotwireTabBarController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ open class HotwireTabBarController: UITabBarController, NavigationHandler {

// MARK: NavigationHandler

open func route(_ url: URL) {
activeNavigator.route(url)
open func route(_ url: URL, options: VisitOptions?, parameters: [String: Any]?) {
activeNavigator.route(url, options: options, parameters: parameters)
}

open func route(_ proposal: VisitProposal) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ final class AppNavigationRouteDecisionHandlerTest: XCTestCase {
startLocation: URL(string: "https://my.app.com")!
)
var route: AppNavigationRouteDecisionHandler!
var navigator: Navigator!
var navigator: NavigationSpy!

override func setUp() {
route = AppNavigationRouteDecisionHandler()
navigator = Navigator(configuration: navigatorConfiguration)
navigator = NavigationSpy()
}

func test_handling_matching_result_navigates() {
Expand Down
8 changes: 4 additions & 4 deletions Tests/Turbo/Routing/RouterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ final class RouterTests: XCTestCase {
)
let url = URL(string: "https://my.app.com/page")!
var router: Router!
var navigator: Navigator!
var navigator: NavigationSpy!

override func setUp() {
navigator = Navigator(configuration: navigatorConfiguration)
navigator = NavigationSpy()
}

func test_no_handlers_stops_navigation() {
Expand Down Expand Up @@ -89,7 +89,7 @@ final class NoMatchRouteDecisionHandlerSpy: RouteDecisionHandler {
return false
}

func handle(location: URL, configuration: HotwireNative.Navigator.Configuration, navigator: HotwireNative.Navigator) -> HotwireNative.Router.Decision {
func handle(location: URL, configuration: HotwireNative.Navigator.Configuration, navigator: HotwireNative.Navigating) -> HotwireNative.Router.Decision {
handleWasCalled = true
return .cancel
}
Expand All @@ -105,7 +105,7 @@ final class MatchRouteDecisionHandlerSpy: RouteDecisionHandler {
return true
}

func handle(location: URL, configuration: HotwireNative.Navigator.Configuration, navigator: HotwireNative.Navigator) -> HotwireNative.Router.Decision {
func handle(location: URL, configuration: HotwireNative.Navigator.Configuration, navigator: HotwireNative.Navigating) -> HotwireNative.Router.Decision {
handleWasCalled = true
return .navigate
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ final class SafariViewControllerRouteDecisionHandlerTests: XCTestCase {
name: "test",
startLocation: URL(string: "https://my.app.com")!
)
var navigator: Navigator!
var navigator: NavigationSpy!
var route: SafariViewControllerRouteDecisionHandler!

override func setUp() {
route = SafariViewControllerRouteDecisionHandler()
navigator = Navigator(configuration: navigatorConfiguration)
navigator = NavigationSpy()
}

func test_handling_matching_result_stops_navigation() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ final class SystemNavigationRouteDecisionHandlerTests: XCTestCase {
name: "test",
startLocation: URL(string: "https://my.app.com")!
)
var navigator: Navigator!
var navigator: NavigationSpy!
var route: SystemNavigationRouteDecisionHandler!

override func setUp() {
route = SystemNavigationRouteDecisionHandler()
navigator = Navigator(configuration: navigatorConfiguration)
navigator = NavigationSpy()
}

func test_handling_matching_result_stops_navigation() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class BaseWebViewPolicyDecisionHandlerTests: XCTestCase {
)

override func setUp() async throws {
navigatorSpy = NavigationSpy(configuration: navigatorConfiguration)
navigatorSpy = NavigationSpy()
webNavigationSimulator = WebViewPolicyNavigationSimulator()
}

Expand Down
29 changes: 20 additions & 9 deletions Tests/Turbo/WebViewPolicy/Spies/NavigationSpy.swift
Original file line number Diff line number Diff line change
@@ -1,25 +1,36 @@
@testable import HotwireNative
import Foundation
import UIKit

final class NavigationSpy: Navigator {
/// A lightweight `Navigating` test double that records navigation calls without
/// instantiating real `Session` or `WKWebView` instances.
final class NavigationSpy: Navigating {
var routeWasCalled = false
var routeURL: URL?
var reloadWasCalled = false

init(configuration: Navigator.Configuration) {
super.init(
session: Session(webView: Hotwire.config.makeWebView()),
modalSession: Session(webView: Hotwire.config.makeWebView()),
configuration: configuration
)
/// Stub for the currently displayed URL, overriding the `Navigating` default.
var currentVisitableURL: URL?

lazy var rootViewController = UINavigationController()
lazy var modalRootViewController = UINavigationController()
lazy var activeNavigationController = UINavigationController()

func route(_ proposal: VisitProposal) {
routeWasCalled = true
routeURL = proposal.url
}

override func route(_ url: URL, options: VisitOptions? = VisitOptions(action: .advance), parameters: [String : Any]? = nil) {
func route(_ url: URL, options: VisitOptions?, parameters: [String: Any]?) {
routeWasCalled = true
routeURL = url
}

override func reload() {
func pop(animated: Bool) {}

func clearAll(animated: Bool) {}

func reload() {
reloadWasCalled = true
}
}
8 changes: 4 additions & 4 deletions Tests/Turbo/WebViewPolicy/WebViewPolicyManagerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ final class WebViewPolicyManagerTests: XCTestCase {
let url = URL(string: "https://my.app.com/page")!
var policyManager: WebViewPolicyManager!
var webNavigationSimulator: WebViewPolicyNavigationSimulator!
var navigator: Navigator!
var navigator: NavigationSpy!
static var navigationAction: WKNavigationAction!

override func setUp() async throws {
navigator = Navigator(configuration: navigatorConfiguration)
navigator = NavigationSpy()

// We can't instantiate a `WKNavigationAction`, so we load HTML and retrieve one.
// In these tests, the navigation action is not actually evaluated, but we need it as an argument
Expand Down Expand Up @@ -112,7 +112,7 @@ final class NoMatchWebViewPolicyDecisionHandler: WebViewPolicyDecisionHandler {

func handle(navigationAction: WKNavigationAction,
configuration: Navigator.Configuration,
navigator: Navigator) -> WebViewPolicyManager.Decision {
navigator: Navigating) -> WebViewPolicyManager.Decision {
handleWasCalled = true
return .cancel
}
Expand All @@ -131,7 +131,7 @@ final class MatchWebViewPolicyDecisionHandler: WebViewPolicyDecisionHandler {

func handle(navigationAction: WKNavigationAction,
configuration: Navigator.Configuration,
navigator: Navigator) -> WebViewPolicyManager.Decision {
navigator: Navigating) -> WebViewPolicyManager.Decision {
handleWasCalled = true
return .cancel
}
Expand Down
Loading