Skip to content

Custom delegate on WKWebView is replaced by mock when snapshotting #440

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
teameh opened this issue Mar 17, 2021 · 13 comments
Closed

Custom delegate on WKWebView is replaced by mock when snapshotting #440

teameh opened this issue Mar 17, 2021 · 13 comments

Comments

@teameh
Copy link
Contributor

teameh commented Mar 17, 2021

When a user sets a custom navigation delegate on a WKWebView and makes a snapshot of a view containing this web view, the navigation delegate may be replaced by a stub that makes sure the snapshotting lib knows when to snapshot the web view. This works great when users don't implement a custom WKNavigationDelegate themselves. But when a user implements a custom delegate themselves this fails.

Consider for example a custom delegate that limits the web view to github.com and opens the browser as soon as the user taps on a link outside of github.com

class GithubNavigationDelegate: NSObject, WKNavigationDelegate {
  override init() { }

  public func webView(
    _ webView: WKWebView,
    decidePolicyFor navigationAction: WKNavigationAction,
    decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
  ) {
    // limit to webpages on to github.com
    guard navigationAction.request.url?.host == "github.com" else {
      decisionHandler(.cancel)
      // Warn user and open url in browser instead
      return
    }

    decisionHandler(.allow)
  }
}


webview.navigationDelegate = GithubNavigationDelegate()

This can't be snapshot tested at the moment because the delegate is removed as soon as a snapshot is made.

I think this could be fixed by retaining the original delegate, implementing all the possible delegate methods and forwarding all the calls to the original delegate. I'll see if I can draft up a PR. Please let me know if you think of another way of dealing with this issue.

Finally, many thanks for all the work on this great lib 👏🏼 so far, hopefully I can contribute back a bit.

@stephencelis
Copy link
Member

That solution sounds great to us! Thanks for raising the issue and we'd love a PR 😄

@teameh
Copy link
Contributor Author

teameh commented Mar 17, 2021

Thanks! That's quick. I'm running the tests on master locally but got lot of failing tests.. any clues?
I'm already running on iPhone 11 Pro Max (13.7)

image

@teameh
Copy link
Contributor Author

teameh commented Mar 17, 2021

Also, do you know why this is necessary?

https://github.com/pointfreeco/swift-snapshot-testing/blob/main/Sources/SnapshotTesting/Common/View.swift#L659-L661

    webView.evaluateJavaScript("document.readyState") { _, _ in
      self.didFinish()
    }

Why execute that javascript, but then no do something with the result. Do you execute it because webkit wait with executing it until the page is loaded? What about a page that never loads, for example a 404?

@teameh
Copy link
Contributor Author

teameh commented Mar 20, 2021

This is harder than I thought because of all the optional methods. You'll need a couple of extra tests as well. PR might take some time.

@Sherlouk
Copy link
Contributor

Sherlouk commented Mar 20, 2021

@teameh Could you do something with forwardingTarget to do without overriding every single method?

This is a partial example from one of my projects 😄 Obviously change the delegate/selector for the one needed by this project. It'll forward every method over automatically.

public final class NetworkProxyDelegate: NSObject, URLSessionTaskDelegate, URLSessionDataDelegate {

    private weak var actualDelegate: URLSessionTaskDelegate?
    private let interceptedSelectors: Set<Selector>

    public init(delegate: URLSessionTaskDelegate?) {
        actualDelegate = delegate
        interceptedSelectors = [
            #selector(URLSessionTaskDelegate.urlSession(_:task:didCompleteWithError:)),
        ]
    }

    // MARK: - URLSessionTaskDelegate

    public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        // custom stuffs

        actualDelegate?.urlSession?(session, task: task, didCompleteWithError: error)
    }

    // MARK: - Proxy
    
    override public func responds(to aSelector: Selector!) -> Bool {
        if interceptedSelectors.contains(aSelector) {
            return true
        }

        return (actualDelegate?.responds(to: aSelector) ?? false) || super.responds(to: aSelector)
    }

    override public func forwardingTarget(for aSelector: Selector!) -> Any? {
        if interceptedSelectors.contains(aSelector) {
            return nil
        }

        return actualDelegate
    }
}

@teameh
Copy link
Contributor Author

teameh commented Mar 21, 2021

@Sherlouk that's a great idea! I've been fighting with this code for a while now and I think I got something that works.

Regarding the line:

return (actualDelegate?.responds(to: aSelector) ?? false) || super.responds(to: aSelector)

The docs say:

You cannot test whether an object inherits a method from its superclass by sending responds(to:) to the object using the super keyword. This method will still be testing the object as a whole, not just the superclass’s implementation. Therefore, sending responds(to:) to super is equivalent to sending it to self. Instead, you must invoke the NSObject class method instancesRespond(to:) directly on the object’s superclass, as illustrated in the following code fragment.

So I think you should use NSObject here:

return (actualDelegate?.responds(to: aSelector) ?? false) || NSObject.responds(to: aSelector)

teameh pushed a commit to teameh/swift-snapshot-testing that referenced this issue Mar 21, 2021
teameh pushed a commit to teameh/swift-snapshot-testing that referenced this issue Mar 21, 2021
teameh pushed a commit to teameh/swift-snapshot-testing that referenced this issue Mar 21, 2021
@teameh
Copy link
Contributor Author

teameh commented Mar 21, 2021

See PR. It might needs to be tweaked a bit more, but it's a start. Thanks for pointing me to responds(to:) @Sherlouk. This is exactly why I love to spend some time on open source projects. I learned a lot today 🙌 .

teameh pushed a commit to teameh/swift-snapshot-testing that referenced this issue Mar 21, 2021
@Sherlouk
Copy link
Contributor

The docs say

Have you got a link to that one please? We've used this code for a while and it's been working fine for us (though admittedly we're not heavy users of the delegate!) - would love to patch this upstream in our project too.

Glad it was helpful and you learned something new 😄 Everyday is a learning opportunity

@teameh
Copy link
Contributor Author

teameh commented Mar 21, 2021

😄

Sure. https://developer.apple.com/documentation/objectivec/nsobjectprotocol/1418583-responds#discussion

These are the docs you get if you option-click the responds(to:) method

teameh pushed a commit to teameh/swift-snapshot-testing that referenced this issue Mar 23, 2021
@teameh
Copy link
Contributor Author

teameh commented Apr 6, 2021

@stephencelis anything I can do to help move this forward?

@EricDeRegter
Copy link

I ran into the same issue.
@teameh any update on this?

@teameh
Copy link
Contributor Author

teameh commented May 3, 2021

@stephencelis anything I can do to help get this merged?

@teameh
Copy link
Contributor Author

teameh commented Aug 17, 2021

For future ref: fixed and merged in #443

mac-gallagher pushed a commit to mac-gallagher/swift-snapshot-testing that referenced this issue Aug 22, 2021
…tfreeco#443)

* Fix pointfreeco#440 - Improve WKWebView navigation delegate

* Fix pointfreeco#440 - Replace WKWebView navigation delegate by observing wkWebView.isLoading

Co-authored-by: Tieme van Veen <[email protected]>
Co-authored-by: Stephen Celis <[email protected]>
niil-qb pushed a commit to quickbit/swift-snapshot-testing that referenced this issue Feb 23, 2023
…tfreeco#443)

* Fix pointfreeco#440 - Improve WKWebView navigation delegate

* Fix pointfreeco#440 - Replace WKWebView navigation delegate by observing wkWebView.isLoading

Co-authored-by: Tieme van Veen <[email protected]>
Co-authored-by: Stephen Celis <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants