Skip to content
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

Add a clearAll method #15

Merged
merged 2 commits into from
Jul 5, 2024
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
7 changes: 7 additions & 0 deletions Examples/Examples/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ struct ContentView: View {
Label("Reload", systemImage: "arrow.clockwise")
.labelStyle(.iconOnly)
}
Button {
proxy.clearAll()
proxy.load(request: viewState.request)
} label: {
Label("Clear", systemImage: "clear")
.labelStyle(.iconOnly)
}
}
.padding(.vertical, 8)

Expand Down
6 changes: 6 additions & 0 deletions Examples/ExamplesUITests/ExamplesUITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ final class ExamplesUITests: XCTestCase {
XCTAssertTrue(app.webViews.staticTexts["0"].waitForExistence(timeout: 3))
}

XCTContext.runActivity(named: "WebViewProxy.clearAll()") { _ in
app.buttons["Clear"].tap()
XCTAssertFalse(app.buttons["Go Back"].isEnabled)
XCTAssertFalse(app.buttons["Go Forward"].isEnabled)
}

XCTContext.runActivity(named: "WebView.uiDelegate(_:)") { _ in
app.webViews.buttons["Confirm"].tap()
XCTAssertTrue(app.alerts.staticTexts["Confirm Test"].waitForExistence(timeout: 3))
Expand Down
48 changes: 48 additions & 0 deletions Sources/WebUI/Remakeable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#if os(iOS)
import UIKit
typealias OSView = UIView
#elseif os(macOS)
import AppKit
typealias OSView = NSView
#endif

final class Remakeable<Content: OSView>: OSView {
private(set) var wrappedValue: Content {
didSet {
action?(wrappedValue)
}
}
private let content: () -> Content
private var action: ((Content) -> Void)?

init(content: @escaping () -> Content) {
self.content = content
wrappedValue = content()
super.init(frame: .zero)
addSubview(wrappedValue)
setConstraints()
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

func remake() {
wrappedValue.removeFromSuperview()
wrappedValue = content()
addSubview(wrappedValue)
setConstraints()
}

func onRemake(perform action: @escaping (Content) -> Void) {
self.action = action
}

private func setConstraints() {
wrappedValue.translatesAutoresizingMaskIntoConstraints = false
wrappedValue.topAnchor.constraint(equalTo: topAnchor).isActive = true
wrappedValue.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
wrappedValue.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
wrappedValue.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
}
}
5 changes: 2 additions & 3 deletions Sources/WebUI/SetUpWebViewProxyAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import SwiftUI
import WebKit

struct SetUpWebViewProxyAction {
let action: @MainActor @Sendable (WKWebView) -> Void
let action: @MainActor @Sendable (Remakeable<EnhancedWKWebView>) -> Void

@MainActor
func callAsFunction(_ webView: WKWebView) {
func callAsFunction(_ webView: Remakeable<EnhancedWKWebView>) {
action(webView)
}
}
Expand All @@ -20,4 +20,3 @@ extension EnvironmentValues {
set { self[SetUpWebViewProxyActionKey.self] = newValue }
}
}

30 changes: 16 additions & 14 deletions Sources/WebUI/WebView+Extension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,34 +18,36 @@ extension WebView: View {
let parent: WebView

@MainActor
private func makeEnhancedWKWebView() -> EnhancedWKWebView {
let webView = EnhancedWKWebView(frame: .zero, configuration: parent.configuration)
private func makeView() -> Remakeable<EnhancedWKWebView> {
let webView = Remakeable {
EnhancedWKWebView(frame: .zero, configuration: parent.configuration)
}
setUpWebViewProxy(webView)
parent.applyModifiers(to: webView)
parent.loadInitialRequest(in: webView)
parent.applyModifiers(to: webView.wrappedValue)
parent.loadInitialRequest(in: webView.wrappedValue)
return webView
}

@MainActor
private func updateEnhancedWKWebView(_ webView: EnhancedWKWebView) {
parent.applyModifiers(to: webView)
private func updateView(_ view: Remakeable<EnhancedWKWebView>) {
parent.applyModifiers(to: view.wrappedValue)
}

#if os(iOS)
func makeUIView(context: Context) -> EnhancedWKWebView {
makeEnhancedWKWebView()
func makeUIView(context: Context) -> Remakeable<EnhancedWKWebView> {
makeView()
}

func updateUIView(_ webView: EnhancedWKWebView, context: Context) {
updateEnhancedWKWebView(webView)
func updateUIView(_ view: Remakeable<EnhancedWKWebView>, context: Context) {
updateView(view)
}
#elseif os(macOS)
func makeNSView(context: Context) -> EnhancedWKWebView {
makeEnhancedWKWebView()
func makeNSView(context: Context) -> Remakeable<EnhancedWKWebView> {
makeView()
}

func updateNSView(_ webView: EnhancedWKWebView, context: Context) {
updateEnhancedWKWebView(webView)
func updateNSView(_ view: Remakeable<EnhancedWKWebView>, context: Context) {
updateView(view)
}
#endif
}
Expand Down
30 changes: 23 additions & 7 deletions Sources/WebUI/WebViewProxy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import WebKit
@available(iOS 16.4, macOS 13.3, *)
@MainActor
public final class WebViewProxy: ObservableObject {
weak var webView: WKWebView?
private(set) weak var webView: Remakeable<EnhancedWKWebView>?

/// The page title.
@Published public private(set) var title: String?
Expand Down Expand Up @@ -37,9 +37,18 @@ public final class WebViewProxy: ObservableObject {
task?.cancel()
}

func setUp(_ webView: WKWebView) {
func setUp(_ webView: Remakeable<EnhancedWKWebView>) {
self.webView = webView
observe(webView.wrappedValue)

webView.onRemake { [weak self] in
guard let self else { return }
observe($0)
}
}

private func observe(_ webView: WKWebView) {
task?.cancel()
task = Task {
await withTaskGroup(of: Void.self) { group in
group.addTask { @MainActor in
Expand Down Expand Up @@ -85,22 +94,22 @@ public final class WebViewProxy: ObservableObject {
/// - Parameters:
/// - request: The request specifying the URL to which to navigate.
public func load(request: URLRequest) {
webView?.load(request)
webView?.wrappedValue.load(request)
}

/// Reloads the current webpage.
public func reload() {
webView?.reload()
webView?.wrappedValue.reload()
}

/// Navigates to the back item in the back-forward list.
public func goBack() {
webView?.goBack()
webView?.wrappedValue.goBack()
}

/// Navigates to the forward item in the back-forward list.
public func goForward() {
webView?.goForward()
webView?.wrappedValue.goForward()
}

/// Evaluates the specified JavaScript string.
Expand Down Expand Up @@ -130,7 +139,7 @@ public final class WebViewProxy: ObservableObject {
public func evaluateJavaScript(_ javaScriptString: String) async throws -> Any? {
guard let webView else { return nil }
return try await withCheckedThrowingContinuation { continuation in
webView.evaluateJavaScript(javaScriptString) { result, error in
webView.wrappedValue.evaluateJavaScript(javaScriptString) { result, error in
if let error {
continuation.resume(throwing: error)
} else {
Expand All @@ -139,4 +148,11 @@ public final class WebViewProxy: ObservableObject {
}
}
}

/// Clears all properties managed by `WKWebView`.
///
/// As a side effect, the WKWebView instance will be remade.
public func clearAll() {
webView?.remake()
}
}
2 changes: 1 addition & 1 deletion Tests/WebUITests/Mock.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
@testable import WebUI
import WebKit

final class WKWebViewMock: EnhancedWKWebView {
final class EnhancedWKWebViewMock: EnhancedWKWebView {
private(set) var loadedRequest: URLRequest?
private(set) var reloadCalled = false
private(set) var goBackCalled = false
Expand Down
46 changes: 36 additions & 10 deletions Tests/WebUITests/WebViewProxyTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,48 +5,74 @@ final class WebViewProxyTests: XCTestCase {
@MainActor
func test_load_the_specified_URLRequest() {
let sut = WebViewProxy()
let webViewMock = WKWebViewMock()
let webViewMock = Remakeable {
EnhancedWKWebViewMock() as EnhancedWKWebView
}
sut.setUp(webViewMock)
let request = URLRequest(url: URL(string: "https://www.example.com")!)
sut.load(request: request)
XCTAssertEqual(webViewMock.loadedRequest, request)
XCTAssertEqual((webViewMock.wrappedValue as! EnhancedWKWebViewMock).loadedRequest, request)
}

@MainActor
func test_reload() {
let sut = WebViewProxy()
let webViewMock = WKWebViewMock()
let webViewMock = Remakeable {
EnhancedWKWebViewMock() as EnhancedWKWebView
}
sut.setUp(webViewMock)
sut.reload()
XCTAssertTrue(webViewMock.reloadCalled)
XCTAssertTrue((webViewMock.wrappedValue as! EnhancedWKWebViewMock).reloadCalled)
}

@MainActor
func test_go_back() {
let sut = WebViewProxy()
let webViewMock = WKWebViewMock()
let webViewMock = Remakeable {
EnhancedWKWebViewMock() as EnhancedWKWebView
}
sut.setUp(webViewMock)
sut.goBack()
XCTAssertTrue(webViewMock.goBackCalled)
XCTAssertTrue((webViewMock.wrappedValue as! EnhancedWKWebViewMock).goBackCalled)
}

@MainActor
func test_go_forward() {
let sut = WebViewProxy()
let webViewMock = WKWebViewMock()
let webViewMock = Remakeable {
EnhancedWKWebViewMock() as EnhancedWKWebView
}
sut.setUp(webViewMock)
sut.goForward()
XCTAssertTrue(webViewMock.goForwardCalled)
XCTAssertTrue((webViewMock.wrappedValue as! EnhancedWKWebViewMock).goForwardCalled)
}

@MainActor
func test_evaluate_JavaScript() async throws {
let sut = WebViewProxy()
let webViewMock = WKWebViewMock()
let webViewMock = Remakeable {
EnhancedWKWebViewMock() as EnhancedWKWebView
}
sut.setUp(webViewMock)
let actual = try await sut.evaluateJavaScript("test")
XCTAssertEqual(webViewMock.javaScriptString, "test")
XCTAssertEqual((webViewMock.wrappedValue as! EnhancedWKWebViewMock).javaScriptString, "test")
let result = try XCTUnwrap(actual as? Bool)
XCTAssertTrue(result)
}

@MainActor
func test_clear_all() async {
let sut = WebViewProxy()
let webViewMock = Remakeable {
EnhancedWKWebViewMock() as EnhancedWKWebView
}
sut.setUp(webViewMock)
let oldInstance = sut.webView?.wrappedValue

sut.clearAll()

let newInstance = sut.webView?.wrappedValue

XCTAssertNotEqual(oldInstance, newInstance)
}
}
16 changes: 8 additions & 8 deletions Tests/WebUITests/WebViewTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ final class WebViewTests: XCTestCase {
func test_applyModifiers_uiDelegate() {
let uiDelegateMock = UIDelegateMock()
let sut = WebView().uiDelegate(uiDelegateMock)
let webViewMock = WKWebViewMock()
let webViewMock = EnhancedWKWebViewMock()
sut.applyModifiers(to: webViewMock)
XCTAssertTrue(uiDelegateMock === webViewMock.uiDelegate)
}
Expand All @@ -15,47 +15,47 @@ final class WebViewTests: XCTestCase {
func test_applyModifiers_navigationDelegate() {
let navigationDelegateMock = NavigationDelegateMock()
let sut = WebView().navigationDelegate(navigationDelegateMock)
let webViewMock = WKWebViewMock()
let webViewMock = EnhancedWKWebViewMock()
sut.applyModifiers(to: webViewMock)
XCTAssertTrue(navigationDelegateMock === webViewMock.navigationDelegateProxy.delegate)
}

@MainActor
func test_applyModifiers_isInspectable() {
let sut = WebView().allowsInspectable(true)
let webViewMock = WKWebViewMock()
let webViewMock = EnhancedWKWebViewMock()
sut.applyModifiers(to: webViewMock)
XCTAssertTrue(webViewMock.isInspectable)
}

@MainActor
func test_applyModifiers_allowsBackForwardNavigationGestures() {
let sut = WebView().allowsBackForwardNavigationGestures(true)
let webViewMock = WKWebViewMock()
let webViewMock = EnhancedWKWebViewMock()
sut.applyModifiers(to: webViewMock)
XCTAssertTrue(webViewMock.allowsBackForwardNavigationGestures)
}

@MainActor
func test_applyModifiers_allowsLinkPreview() {
let sut = WebView().allowsLinkPreview(true)
let webViewMock = WKWebViewMock()
let webViewMock = EnhancedWKWebViewMock()
sut.applyModifiers(to: webViewMock)
XCTAssertTrue(webViewMock.allowsLinkPreview)
}

@MainActor
func test_applyModifiers_isRefreshable() {
let sut = WebView().refreshable()
let webViewMock = WKWebViewMock()
let webViewMock = EnhancedWKWebViewMock()
sut.applyModifiers(to: webViewMock)
XCTAssertTrue(webViewMock.isRefreshable)
}

@MainActor
func test_loadInitialRequest_do_not_load_URL_request_if_request_is_not_specified_in_init() {
let sut = WebView()
let webViewMock = WKWebViewMock()
let webViewMock = EnhancedWKWebViewMock()
sut.loadInitialRequest(in: webViewMock)
XCTAssertNil(webViewMock.loadedRequest)
}
Expand All @@ -64,7 +64,7 @@ final class WebViewTests: XCTestCase {
func test_loadInitialRequest_load_URL_request_if_request_is_specified_in_init() {
let request = URLRequest(url: URL(string: "https://www.example.com")!)
let sut = WebView(request: request)
let webViewMock = WKWebViewMock()
let webViewMock = EnhancedWKWebViewMock()
sut.loadInitialRequest(in: webViewMock)
XCTAssertEqual(webViewMock.loadedRequest, request)
}
Expand Down
Loading