From 9880c97f410de422ef840729329c2084061ef38a Mon Sep 17 00:00:00 2001 From: e271828- Date: Fri, 20 Mar 2026 13:04:11 -0400 Subject: [PATCH 1/3] fix flaky bench completion handling --- .../Core/HCaptchaWebViewManager__Tests.swift | 21 +++++++++++ .../HCaptcha_Tests/Core/HCaptcha__Bench.swift | 7 ++++ .../HCaptchaWebViewManager+Private.swift | 35 +++++++++++++------ ...aWebViewManager+WKNavigationDelegate.swift | 6 ++-- 4 files changed, 56 insertions(+), 13 deletions(-) diff --git a/Example/HCaptcha_Tests/Core/HCaptchaWebViewManager__Tests.swift b/Example/HCaptcha_Tests/Core/HCaptchaWebViewManager__Tests.swift index 38307fc8..49c717a1 100644 --- a/Example/HCaptcha_Tests/Core/HCaptchaWebViewManager__Tests.swift +++ b/Example/HCaptcha_Tests/Core/HCaptchaWebViewManager__Tests.swift @@ -77,6 +77,27 @@ class HCaptchaWebViewManager__Tests: XCTestCase { XCTAssertEqual(result2?.token, apiKey) } + func test__Validate__Ignores_Late_Termination_After_Token() { + let exp = expectation(description: "load token") + var results = [HCaptchaResult]() + + let manager = HCaptchaWebViewManager(messageBody: "{token: key}", apiKey: apiKey) + manager.validate(on: presenterView) { response in + results.append(response) + if results.count == 1 { + exp.fulfill() + } + } + + waitForExpectations(timeout: TestTimeouts.standard) + + manager.webViewWebContentProcessDidTerminate(manager.webView) + + XCTAssertEqual(results.count, 1) + XCTAssertNil(results.first?.error) + XCTAssertEqual(results.first?.token, apiKey) + } + func test__Validate__Show_HCaptcha() { let exp = expectation(description: "show hcaptcha") diff --git a/Example/HCaptcha_Tests/Core/HCaptcha__Bench.swift b/Example/HCaptcha_Tests/Core/HCaptcha__Bench.swift index 2ca98c4a..9b3a3c1a 100644 --- a/Example/HCaptcha_Tests/Core/HCaptcha__Bench.swift +++ b/Example/HCaptcha_Tests/Core/HCaptcha__Bench.swift @@ -85,6 +85,13 @@ class HCaptcha__Bench: XCTestCase { func testBenchVerify() throws { let view = UIView(frame: CGRect(x: 0, y: 0, width: 400, height: 600)) let hcaptcha = try? HCaptcha(apiKey: apiKey, size: .invisible) + let loaded = expectation(description: "loaded") + + hcaptcha?.didFinishLoading { + loaded.fulfill() + } + wait(for: [loaded], timeout: TestTimeouts.long) + self.measureMetrics([.wallClockTime], automaticallyStartMeasuring: true, for: { let exp = expectation(description: "completed") hcaptcha?.validate(on: view, completion: { result in diff --git a/HCaptcha/Classes/HCaptchaWebViewManager+Private.swift b/HCaptcha/Classes/HCaptchaWebViewManager+Private.swift index 9bca5b66..4d9af18d 100644 --- a/HCaptcha/Classes/HCaptchaWebViewManager+Private.swift +++ b/HCaptcha/Classes/HCaptchaWebViewManager+Private.swift @@ -37,15 +37,18 @@ extension HCaptchaWebViewManager { func handle(result: HCaptchaDecoder.Result) { Log.debug("WebViewManager.handleResult: \(result)") - guard !resultHandled else { - Log.debug("WebViewManager.handleResult skip as handled") - return - } - switch result { case .token(let token): - completion?(HCaptchaResult(self, token: token)) + guard !resultHandled else { + Log.debug("WebViewManager.handleResult skip token as handled") + return + } + complete(HCaptchaResult(self, token: token)) case .error(let error): + guard !resultHandled else { + Log.debug("WebViewManager.handleResult skip error as handled") + return + } handle(error: error) onEvent?(.error, error) case .showHCaptcha: webView.isHidden = false @@ -66,11 +69,11 @@ extension HCaptchaWebViewManager { reset() validate(on: view) } else { - completion?(HCaptchaResult(self, error: error)) + complete(HCaptchaResult(self, error: error)) } } else { - if let completion = completion { - completion(HCaptchaResult(self, error: error)) + if completion != nil { + complete(HCaptchaResult(self, error: error)) } else { lastError = error } @@ -172,8 +175,8 @@ extension HCaptchaWebViewManager { guard let self = self else { return } Log.debug("WebViewManager complete with pendingError: \(error)") - self.completion?(HCaptchaResult(self, error: error)) self.lastError = nil + self.complete(HCaptchaResult(self, error: error)) } if error == .networkError { Log.debug("WebViewManager reloads html after \(error) error") @@ -196,4 +199,16 @@ extension HCaptchaWebViewManager { func executeJS(command: JSCommand) { executeJS(command: command, didLoad: self.didFinishLoading) } + + func complete(_ result: HCaptchaResult) { + guard !resultHandled else { + Log.debug("WebViewManager.complete skip as handled") + return + } + + resultHandled = true + let completion = self.completion + self.completion = nil + completion?(result) + } } diff --git a/HCaptcha/Classes/HCaptchaWebViewManager+WKNavigationDelegate.swift b/HCaptcha/Classes/HCaptchaWebViewManager+WKNavigationDelegate.swift index ae4afac3..a3cb7bb6 100644 --- a/HCaptcha/Classes/HCaptchaWebViewManager+WKNavigationDelegate.swift +++ b/HCaptcha/Classes/HCaptchaWebViewManager+WKNavigationDelegate.swift @@ -30,13 +30,13 @@ extension HCaptchaWebViewManager: WKNavigationDelegate, WKUIDelegate { /// Tells the delegate that an error occurred during navigation. func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { Log.debug("WebViewManager.webViewDidFail with \(error)") - completion?(HCaptchaResult(self, error: .unexpected(error))) + complete(HCaptchaResult(self, error: .unexpected(error))) } /// Tells the delegate that an error occurred during the early navigation process. func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { Log.debug("WebViewManager.webViewDidFailProvisionalNavigation with \(error)") - completion?(HCaptchaResult(self, error: .unexpected(error))) + complete(HCaptchaResult(self, error: .unexpected(error))) } /// Tells the delegate that the web view’s content process was terminated. @@ -49,7 +49,7 @@ extension HCaptchaWebViewManager: WKNavigationDelegate, WKUIDelegate { userInfo: [ NSLocalizedDescriptionKey: "WebView web content process did terminate", NSLocalizedRecoverySuggestionErrorKey: "Call HCaptcha.reset()"]) - completion?(HCaptchaResult(self, error: .unexpected(error))) didFinishLoading = false + complete(HCaptchaResult(self, error: .unexpected(error))) } } From 74ae6257acb994954739ee70f7980acc1b4785d3 Mon Sep 17 00:00:00 2001 From: Aliaksandr Babrykovich Date: Sat, 21 Mar 2026 22:59:34 +0100 Subject: [PATCH 2/3] fix: reduce cyclomatic complexity in handle(result:) --- .../HCaptchaWebViewManager+Private.swift | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/HCaptcha/Classes/HCaptchaWebViewManager+Private.swift b/HCaptcha/Classes/HCaptchaWebViewManager+Private.swift index 4d9af18d..107ca7ba 100644 --- a/HCaptcha/Classes/HCaptchaWebViewManager+Private.swift +++ b/HCaptcha/Classes/HCaptchaWebViewManager+Private.swift @@ -38,19 +38,8 @@ extension HCaptchaWebViewManager { Log.debug("WebViewManager.handleResult: \(result)") switch result { - case .token(let token): - guard !resultHandled else { - Log.debug("WebViewManager.handleResult skip token as handled") - return - } - complete(HCaptchaResult(self, token: token)) - case .error(let error): - guard !resultHandled else { - Log.debug("WebViewManager.handleResult skip error as handled") - return - } - handle(error: error) - onEvent?(.error, error) + case .token(let token): handleToken(token) + case .error(let error): handleDecoderError(error) case .showHCaptcha: webView.isHidden = false case .didLoad: didLoad() case .onOpen: onEvent?(.open, nil) @@ -61,6 +50,23 @@ extension HCaptchaWebViewManager { } } + private func handleToken(_ token: String) { + guard !resultHandled else { + Log.debug("WebViewManager.handleResult skip token as handled") + return + } + complete(HCaptchaResult(self, token: token)) + } + + private func handleDecoderError(_ error: HCaptchaError) { + guard !resultHandled else { + Log.debug("WebViewManager.handleResult skip error as handled") + return + } + handle(error: error) + onEvent?(.error, error) + } + private func handle(error: HCaptchaError) { loadingTimer?.invalidate() loadingTimer = nil From 8bfaa1ba3ea095cf21705135086e5dbe64145d3b Mon Sep 17 00:00:00 2001 From: e271828- Date: Mon, 23 Mar 2026 18:48:50 -0400 Subject: [PATCH 3/3] fix: narrow completion race handling --- Example/HCaptcha_Tests/Core/HCaptcha__Bench.swift | 7 ------- HCaptcha/Classes/HCaptchaWebViewManager+Private.swift | 6 ------ HCaptcha/Classes/HCaptchaWebViewManager.swift | 3 ++- 3 files changed, 2 insertions(+), 14 deletions(-) diff --git a/Example/HCaptcha_Tests/Core/HCaptcha__Bench.swift b/Example/HCaptcha_Tests/Core/HCaptcha__Bench.swift index 9b3a3c1a..2ca98c4a 100644 --- a/Example/HCaptcha_Tests/Core/HCaptcha__Bench.swift +++ b/Example/HCaptcha_Tests/Core/HCaptcha__Bench.swift @@ -85,13 +85,6 @@ class HCaptcha__Bench: XCTestCase { func testBenchVerify() throws { let view = UIView(frame: CGRect(x: 0, y: 0, width: 400, height: 600)) let hcaptcha = try? HCaptcha(apiKey: apiKey, size: .invisible) - let loaded = expectation(description: "loaded") - - hcaptcha?.didFinishLoading { - loaded.fulfill() - } - wait(for: [loaded], timeout: TestTimeouts.long) - self.measureMetrics([.wallClockTime], automaticallyStartMeasuring: true, for: { let exp = expectation(description: "completed") hcaptcha?.validate(on: view, completion: { result in diff --git a/HCaptcha/Classes/HCaptchaWebViewManager+Private.swift b/HCaptcha/Classes/HCaptchaWebViewManager+Private.swift index 107ca7ba..3ddee622 100644 --- a/HCaptcha/Classes/HCaptchaWebViewManager+Private.swift +++ b/HCaptcha/Classes/HCaptchaWebViewManager+Private.swift @@ -207,12 +207,6 @@ extension HCaptchaWebViewManager { } func complete(_ result: HCaptchaResult) { - guard !resultHandled else { - Log.debug("WebViewManager.complete skip as handled") - return - } - - resultHandled = true let completion = self.completion self.completion = nil completion?(result) diff --git a/HCaptcha/Classes/HCaptchaWebViewManager.swift b/HCaptcha/Classes/HCaptchaWebViewManager.swift index f7076a09..bfef5d8f 100644 --- a/HCaptcha/Classes/HCaptchaWebViewManager.swift +++ b/HCaptcha/Classes/HCaptchaWebViewManager.swift @@ -154,7 +154,7 @@ internal class HCaptchaWebViewManager: NSObject { if !passiveApiKey { guard let view = view else { - completion?(HCaptchaResult(self, error: .failedSetup)) + complete(HCaptchaResult(self, error: .failedSetup)) return } @@ -171,6 +171,7 @@ internal class HCaptchaWebViewManager: NSObject { func stop() { Log.debug("WebViewManager.stop") stopInitWebViewConfiguration = true + completion = nil webView.stopLoading() resultHandled = true loadingTimer?.invalidate()