Skip to content

Commit

Permalink
Merge pull request #15 from cybozu/add-clear-all-method
Browse files Browse the repository at this point in the history
Add a clearAll method
  • Loading branch information
Kyome22 authored Jul 5, 2024
2 parents f1ea230 + 8db61f3 commit 4708709
Show file tree
Hide file tree
Showing 9 changed files with 147 additions and 43 deletions.
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

0 comments on commit 4708709

Please sign in to comment.