From ca6d8963f45b32b526bd46ae0e478233830782c8 Mon Sep 17 00:00:00 2001 From: ichiho-ojima Date: Sat, 15 Jun 2024 00:24:59 +0900 Subject: [PATCH] Add a clearAll method --- Examples/Examples/ContentView.swift | 2 +- .../ExamplesUITests/ExamplesUITests.swift | 6 ++ Sources/WebUI/EnhancedWKWebViewWrapper.swift | 39 ------------- Sources/WebUI/Remakeable.swift | 48 +++++++++++++++ Sources/WebUI/SetUpWebViewProxyAction.swift | 8 +-- Sources/WebUI/WebView+Extension.swift | 26 ++++----- Sources/WebUI/WebViewProxy.swift | 41 ++++++------- Sources/WebUI/WebViewReader.swift | 2 +- Tests/WebUITests/Mock.swift | 2 +- Tests/WebUITests/WebViewProxyTests.swift | 58 ++++++++++++------- Tests/WebUITests/WebViewTests.swift | 16 ++--- 11 files changed, 137 insertions(+), 111 deletions(-) delete mode 100644 Sources/WebUI/EnhancedWKWebViewWrapper.swift create mode 100644 Sources/WebUI/Remakeable.swift diff --git a/Examples/Examples/ContentView.swift b/Examples/Examples/ContentView.swift index 8c73d4a..09aadbe 100644 --- a/Examples/Examples/ContentView.swift +++ b/Examples/Examples/ContentView.swift @@ -35,7 +35,7 @@ struct ContentView: View { .labelStyle(.iconOnly) } Button { - proxy.clearHistory() + proxy.clearAll() proxy.load(request: viewState.request) } label: { Label("Clear", systemImage: "clear") diff --git a/Examples/ExamplesUITests/ExamplesUITests.swift b/Examples/ExamplesUITests/ExamplesUITests.swift index c405f32..98d73b3 100644 --- a/Examples/ExamplesUITests/ExamplesUITests.swift +++ b/Examples/ExamplesUITests/ExamplesUITests.swift @@ -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)) diff --git a/Sources/WebUI/EnhancedWKWebViewWrapper.swift b/Sources/WebUI/EnhancedWKWebViewWrapper.swift deleted file mode 100644 index a34953e..0000000 --- a/Sources/WebUI/EnhancedWKWebViewWrapper.swift +++ /dev/null @@ -1,39 +0,0 @@ -import SwiftUI -import WebKit - -#if os(iOS) -typealias OSView = UIView -#elseif os(macOS) -typealias OSView = NSView -#endif - -final class EnhancedWKWebViewWrapper: OSView { - var webView: EnhancedWKWebView - - init(configuration: WKWebViewConfiguration) { - webView = EnhancedWKWebView(frame: .zero, configuration: configuration) - super.init(frame: .zero) - addSubview(webView) - setConstraints() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func remakeWebView(configuration: WKWebViewConfiguration) -> WKWebView { - webView.removeFromSuperview() - webView = EnhancedWKWebView(frame: .zero, configuration: configuration) - addSubview(webView) - setConstraints() - return webView - } - - private func setConstraints() { - webView.translatesAutoresizingMaskIntoConstraints = false - webView.topAnchor.constraint(equalTo: topAnchor).isActive = true - webView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true - webView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true - webView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true - } -} diff --git a/Sources/WebUI/Remakeable.swift b/Sources/WebUI/Remakeable.swift new file mode 100644 index 0000000..fb4a2e7 --- /dev/null +++ b/Sources/WebUI/Remakeable.swift @@ -0,0 +1,48 @@ +#if os(iOS) +import UIKit +typealias OSView = UIView +#elseif os(macOS) +import AppKit +typealias OSView = NSView +#endif + +final class Remakeable: 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 + } +} diff --git a/Sources/WebUI/SetUpWebViewProxyAction.swift b/Sources/WebUI/SetUpWebViewProxyAction.swift index fc64a37..3d76591 100644 --- a/Sources/WebUI/SetUpWebViewProxyAction.swift +++ b/Sources/WebUI/SetUpWebViewProxyAction.swift @@ -2,16 +2,16 @@ import SwiftUI import WebKit struct SetUpWebViewProxyAction { - let action: @MainActor @Sendable (WKWebView, @escaping @MainActor @Sendable () -> WKWebView) -> Void + let action: @MainActor @Sendable (Remakeable) -> Void @MainActor - func callAsFunction(_ webView: WKWebView, _ remakeHandler: @escaping @MainActor @Sendable () -> WKWebView) { - action(webView, remakeHandler) + func callAsFunction(_ webView: Remakeable) { + action(webView) } } private struct SetUpWebViewProxyActionKey: EnvironmentKey { - static let defaultValue = SetUpWebViewProxyAction(action: { _, _ in }) + static let defaultValue = SetUpWebViewProxyAction(action: { _ in }) } extension EnvironmentValues { diff --git a/Sources/WebUI/WebView+Extension.swift b/Sources/WebUI/WebView+Extension.swift index c25a38e..dc23a96 100644 --- a/Sources/WebUI/WebView+Extension.swift +++ b/Sources/WebUI/WebView+Extension.swift @@ -18,35 +18,35 @@ extension WebView: View { let parent: WebView @MainActor - private func makeView() -> EnhancedWKWebViewWrapper { - let view = EnhancedWKWebViewWrapper(configuration: parent.configuration) - setUpWebViewProxy(view.webView) { - view.remakeWebView(configuration: parent.configuration) + private func makeView() -> Remakeable { + let webView = Remakeable { + EnhancedWKWebView(frame: .zero, configuration: parent.configuration) } - parent.applyModifiers(to: view.webView) - parent.loadInitialRequest(in: view.webView) - return view + setUpWebViewProxy(webView) + parent.applyModifiers(to: webView.wrappedValue) + parent.loadInitialRequest(in: webView.wrappedValue) + return webView } @MainActor - private func updateView(_ view: EnhancedWKWebViewWrapper) { - parent.applyModifiers(to: view.webView) + private func updateView(_ view: Remakeable) { + parent.applyModifiers(to: view.wrappedValue) } #if os(iOS) - func makeUIView(context: Context) -> EnhancedWKWebViewWrapper { + func makeUIView(context: Context) -> Remakeable { makeView() } - func updateUIView(_ view: EnhancedWKWebViewWrapper, context: Context) { + func updateUIView(_ view: Remakeable, context: Context) { updateView(view) } #elseif os(macOS) - func makeNSView(context: Context) -> EnhancedWKWebViewWrapper { + func makeNSView(context: Context) -> Remakeable { makeView() } - func updateNSView(_ view: EnhancedWKWebViewWrapper, context: Context) { + func updateNSView(_ view: Remakeable, context: Context) { updateView(view) } #endif diff --git a/Sources/WebUI/WebViewProxy.swift b/Sources/WebUI/WebViewProxy.swift index cf6be5c..fbd1c65 100644 --- a/Sources/WebUI/WebViewProxy.swift +++ b/Sources/WebUI/WebViewProxy.swift @@ -9,8 +9,7 @@ import WebKit @available(iOS 16.4, macOS 13.3, *) @MainActor public final class WebViewProxy: ObservableObject { - private weak var webView: WKWebView? - private var remakeHandler: (() -> WKWebView)? + private(set) weak var webView: Remakeable? /// The page title. @Published public private(set) var title: String? @@ -38,14 +37,18 @@ public final class WebViewProxy: ObservableObject { task?.cancel() } - func setUp(_ webView: WKWebView, _ remakeHandler: @escaping () -> WKWebView) { - setUpWebView(webView) - setUpRemakeHandler(remakeHandler) - } - - func setUpWebView(_ webView: WKWebView) { + func setUp(_ webView: Remakeable) { 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 @@ -87,30 +90,26 @@ public final class WebViewProxy: ObservableObject { } } - func setUpRemakeHandler(_ remakeHandler: @escaping () -> WKWebView) { - self.remakeHandler = remakeHandler - } - /// Navigates to a requested URL. /// - 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. @@ -140,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 { @@ -150,12 +149,10 @@ public final class WebViewProxy: ObservableObject { } } - /// Clears all history. + /// Clears history and cookies. /// /// As a side effect, the WKWebView instance will be remade. - public func clearHistory() { - guard let webView = remakeHandler?() else { return } - task?.cancel() - setUpWebView(webView) + public func clearAll() { + webView?.remake() } } diff --git a/Sources/WebUI/WebViewReader.swift b/Sources/WebUI/WebViewReader.swift index ae5a385..96b07a3 100644 --- a/Sources/WebUI/WebViewReader.swift +++ b/Sources/WebUI/WebViewReader.swift @@ -35,7 +35,7 @@ public struct WebViewReader: View { public var body: some View { content(proxy) .environment(\.setUpWebViewProxy, SetUpWebViewProxyAction(action: { - proxy.setUp($0, $1) + proxy.setUp($0) })) } } diff --git a/Tests/WebUITests/Mock.swift b/Tests/WebUITests/Mock.swift index 76ee404..c7ba285 100644 --- a/Tests/WebUITests/Mock.swift +++ b/Tests/WebUITests/Mock.swift @@ -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 diff --git a/Tests/WebUITests/WebViewProxyTests.swift b/Tests/WebUITests/WebViewProxyTests.swift index f09a110..6735e79 100644 --- a/Tests/WebUITests/WebViewProxyTests.swift +++ b/Tests/WebUITests/WebViewProxyTests.swift @@ -5,60 +5,74 @@ final class WebViewProxyTests: XCTestCase { @MainActor func test_load_the_specified_URLRequest() { let sut = WebViewProxy() - let webViewMock = WKWebViewMock() - sut.setUpWebView(webViewMock) + 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() - sut.setUpWebView(webViewMock) + 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() - sut.setUpWebView(webViewMock) + 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() - sut.setUpWebView(webViewMock) + 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() - sut.setUpWebView(webViewMock) + 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_history() async { + func test_clear_all() async { let sut = WebViewProxy() - let expectation = XCTestExpectation() - sut.setUpRemakeHandler { - expectation.fulfill() - return WKWebViewMock() + let webViewMock = Remakeable { + EnhancedWKWebViewMock() as EnhancedWKWebView } - sut.clearHistory() - await fulfillment(of: [expectation], timeout: 0.1) + sut.setUp(webViewMock) + let oldInstance = sut.webView?.wrappedValue + + sut.clearAll() + + let newInstance = sut.webView?.wrappedValue + + XCTAssertNotEqual(oldInstance, newInstance) } } diff --git a/Tests/WebUITests/WebViewTests.swift b/Tests/WebUITests/WebViewTests.swift index 66bd185..75d788f 100644 --- a/Tests/WebUITests/WebViewTests.swift +++ b/Tests/WebUITests/WebViewTests.swift @@ -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) } @@ -15,7 +15,7 @@ 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) } @@ -23,7 +23,7 @@ final class WebViewTests: XCTestCase { @MainActor func test_applyModifiers_isInspectable() { let sut = WebView().allowsInspectable(true) - let webViewMock = WKWebViewMock() + let webViewMock = EnhancedWKWebViewMock() sut.applyModifiers(to: webViewMock) XCTAssertTrue(webViewMock.isInspectable) } @@ -31,7 +31,7 @@ final class WebViewTests: XCTestCase { @MainActor func test_applyModifiers_allowsBackForwardNavigationGestures() { let sut = WebView().allowsBackForwardNavigationGestures(true) - let webViewMock = WKWebViewMock() + let webViewMock = EnhancedWKWebViewMock() sut.applyModifiers(to: webViewMock) XCTAssertTrue(webViewMock.allowsBackForwardNavigationGestures) } @@ -39,7 +39,7 @@ final class WebViewTests: XCTestCase { @MainActor func test_applyModifiers_allowsLinkPreview() { let sut = WebView().allowsLinkPreview(true) - let webViewMock = WKWebViewMock() + let webViewMock = EnhancedWKWebViewMock() sut.applyModifiers(to: webViewMock) XCTAssertTrue(webViewMock.allowsLinkPreview) } @@ -47,7 +47,7 @@ final class WebViewTests: XCTestCase { @MainActor func test_applyModifiers_isRefreshable() { let sut = WebView().refreshable() - let webViewMock = WKWebViewMock() + let webViewMock = EnhancedWKWebViewMock() sut.applyModifiers(to: webViewMock) XCTAssertTrue(webViewMock.isRefreshable) } @@ -55,7 +55,7 @@ final class WebViewTests: XCTestCase { @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) } @@ -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) }