diff --git a/Source/Turbo/WebView/ScriptMessage.swift b/Source/Turbo/WebView/ScriptMessage.swift index b3d8e2f..b349c56 100644 --- a/Source/Turbo/WebView/ScriptMessage.swift +++ b/Source/Turbo/WebView/ScriptMessage.swift @@ -54,6 +54,7 @@ extension ScriptMessage { enum Name: String { case pageLoaded case pageLoadFailed + case turboIsReady case errorRaised case visitProposed case visitProposalScrollingToAnchor diff --git a/Source/Turbo/WebView/WebViewBridge.swift b/Source/Turbo/WebView/WebViewBridge.swift index de6a7e3..c96c1b8 100644 --- a/Source/Turbo/WebView/WebViewBridge.swift +++ b/Source/Turbo/WebView/WebViewBridge.swift @@ -112,6 +112,11 @@ extension WebViewBridge: ScriptMessageHandlerDelegate { switch message.name { case .pageLoaded: pageLoadDelegate?.webView(self, didLoadPageWithRestorationIdentifier: message.restorationIdentifier!) + case .turboIsReady: + let isReady = message.data["isReady"] as? Bool ?? false + if !isReady { + delegate?.webView(self, didFailInitialPageLoadWithError: .load(.notReady)) + } case .pageLoadFailed: delegate?.webView(self, didFailInitialPageLoadWithError: .load(.notPresent)) case .formSubmissionStarted: diff --git a/Source/Turbo/WebView/turbo.js b/Source/Turbo/WebView/turbo.js index d22993d..3d51895 100644 --- a/Source/Turbo/WebView/turbo.js +++ b/Source/Turbo/WebView/turbo.js @@ -11,8 +11,10 @@ registerAdapter() { if (window.Turbo) { Turbo.registerAdapter(this) + this.turboIsReady(true) } else if (window.Turbolinks) { Turbolinks.controller.adapter = this + this.turboIsReady(true) } else { throw new Error("Failed to register the TurboNative adapter") } @@ -30,6 +32,10 @@ this.postMessageAfterNextRepaint("pageLoaded", { restorationIdentifier }) } + turboIsReady(isReady) { + this.postMessage("turboIsReady", { isReady: isReady }) + } + pageLoadFailed() { this.postMessage("pageLoadFailed") } @@ -231,6 +237,7 @@ setTimeout(() => { if (!window.Turbo && !window.Turbolinks) { + window.turboNative.turboIsReady(false) window.turboNative.pageLoadFailed() } }, TURBO_LOAD_TIMEOUT) diff --git a/Tests/Turbo/ScriptMessageTests.swift b/Tests/Turbo/ScriptMessageTests.swift index 8847a07..3603551 100644 --- a/Tests/Turbo/ScriptMessageTests.swift +++ b/Tests/Turbo/ScriptMessageTests.swift @@ -37,6 +37,24 @@ class ScriptMessageTests: XCTestCase { let message = ScriptMessage(message: script) XCTAssertNil(message) } + + func test_parse_turboIsReady_withFalse_returnsMessage() throws { + let data: [String: Any] = ["isReady": false, "timestamp": 0] + let script = FakeScriptMessage(body: ["name": "turboIsReady", "data": data] as [String: Any]) + + let message = try XCTUnwrap(ScriptMessage(message: script)) + XCTAssertEqual(message.name, .turboIsReady) + XCTAssertEqual(message.data["isReady"] as? Bool, false) + } + + func test_parse_turboIsReady_withTrue_returnsMessage() throws { + let data: [String: Any] = ["isReady": true, "timestamp": 0] + let script = FakeScriptMessage(body: ["name": "turboIsReady", "data": data] as [String: Any]) + + let message = try XCTUnwrap(ScriptMessage(message: script)) + XCTAssertEqual(message.name, .turboIsReady) + XCTAssertEqual(message.data["isReady"] as? Bool, true) + } } // Can't instantiate a WKScriptMessage directly diff --git a/Tests/Turbo/SessionTests.swift b/Tests/Turbo/SessionTests.swift index 3782610..812a59e 100644 --- a/Tests/Turbo/SessionTests.swift +++ b/Tests/Turbo/SessionTests.swift @@ -95,15 +95,19 @@ class SessionTests: XCTestCase { } @MainActor - func test_coldBootVisit_whenVisitFailsFromMissingLibrary_providesAnPageLoadError() async throws { - await visit("/missing-library", timeout: turboTimeout + defaultTimeout) + func test_coldBootVisit_whenVisitFailsFromMissingLibrary_providesNotReadyAndNotPresentErrors() async throws { + let expectation = self.expectation(description: "Wait for both load errors.") + expectation.expectedFulfillmentCount = 2 + sessionDelegate.didChange = { expectation.fulfill() } - XCTAssertTrue(sessionDelegate.sessionDidFailRequestCalled) - XCTAssertTrue(sessionDelegate.sessionDidFinishRequestCalled) + let visitable = TestVisitable(url: url("/missing-library")) + session.visit(visitable) + await fulfillment(of: [expectation], timeout: turboTimeout + defaultTimeout) - XCTAssertNotNil(sessionDelegate.failedRequestError) - let error = try XCTUnwrap(sessionDelegate.failedRequestError) - XCTAssertEqual(error, .load(.notPresent)) + XCTAssertTrue(sessionDelegate.sessionDidFailRequestCalled) + XCTAssertEqual(sessionDelegate.allFailedRequestErrors.count, 2) + XCTAssertEqual(sessionDelegate.allFailedRequestErrors[0], .load(.notReady)) + XCTAssertEqual(sessionDelegate.allFailedRequestErrors[1], .load(.notPresent)) } // MARK: - Server diff --git a/Tests/Turbo/Test.swift b/Tests/Turbo/Test.swift index c44d6e4..06e424c 100644 --- a/Tests/Turbo/Test.swift +++ b/Tests/Turbo/Test.swift @@ -55,6 +55,7 @@ class TestSessionDelegate: NSObject, SessionDelegate { var sessionDidStartRequestCalled = false var sessionDidFinishRequestCalled = false var failedRequestError: HotwireNativeError? = nil + var allFailedRequestErrors: [HotwireNativeError] = [] var sessionDidFailRequestCalled = false { didSet { didChange?() }} var sessionDidProposeVisitCalled = false var sessionDidProposeVisitToCrossOriginRedirectWasCalled = false @@ -83,6 +84,7 @@ class TestSessionDelegate: NSObject, SessionDelegate { func session(_ session: Session, didFailRequestForVisitable visitable: Visitable, error: HotwireNativeError) { sessionDidFailRequestCalled = true failedRequestError = error + allFailedRequestErrors.append(error) } func session(_ session: Session, didProposeVisit proposal: VisitProposal) {