Ignore history items added by JS without user interaction when navigation back/forward via the WKWebView API
https://bugs.webkit.org/show_bug.cgi?id=241885
<rdar://94838657>

Reviewed by Geoffrey Garen.

Ignore history items added by JS without user interaction when navigation
back/forward via the WKWebView API. This is a behavior similar to the
intervention made in Chrome (https://bugs.chromium.org/p/chromium/issues/detail?id=907167)
to prevent websites from hijacking the back/forward list.

When an history item is added by JS via history.pushState() and without a user
gesture, we now set a flag on that HistoryItem to remember this. Later on, when
calling [WKWebView goBack] or [WKWebView goForward], we will skip the history
item that have this flag set. This behavior occurs behind a linked-on-after
check to reduce the compatibility risk.

Also, navigations via other means (e.g. via JavaScript) are not impacted and will
ignore this new flag.

* Source/WTF/wtf/cocoa/RuntimeApplicationChecksCocoa.h:
* Source/WebCore/history/HistoryItem.h:
(WebCore::HistoryItem::setWasCreatedByJSWithoutUserInteraction):
(WebCore::HistoryItem::wasCreatedByJSWithoutUserInteraction const):
* Source/WebCore/loader/HistoryController.cpp:
(WebCore::FrameLoader::HistoryController::pushState):
* Source/WebKit/Shared/SessionState.cpp:
(WebKit::PageState::encode const):
(WebKit::PageState::decode):
* Source/WebKit/Shared/SessionState.h:
* Source/WebKit/Shared/WebBackForwardListItem.h:
(WebKit::WebBackForwardListItem::wasCreatedByJSWithoutUserInteraction const):
* Source/WebKit/UIProcess/WebPageProxy.cpp:
(WebKit::itemSkippingBackForwardItemsAddedByJSWithoutUserGesture):
(WebKit::WebPageProxy::goForward):
(WebKit::WebPageProxy::goBack):
* Source/WebKit/WebProcess/WebCoreSupport/SessionStateConversion.cpp:
(WebKit::toBackForwardListItemState):
* Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj:
* Tools/TestWebKitAPI/Tests/WebKit/WKBackForwardList.mm:
(TEST):
* Tools/TestWebKitAPI/cocoa/TestNavigationDelegate.h:
* Tools/TestWebKitAPI/cocoa/TestNavigationDelegate.mm:
(-[TestNavigationDelegate _webView:navigation:didSameDocumentNavigation:]):
(-[TestNavigationDelegate waitForDidFinishNavigationOrSameDocumentNavigation]):
(-[WKWebView _test_waitForDidFinishNavigationOrSameDocumentNavigation]):

Canonical link: https://commits.webkit.org/251783@main


git-svn-id: http://svn.webkit.org/repository/webkit/trunk@295778 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/Source/WTF/wtf/cocoa/RuntimeApplicationChecksCocoa.h b/Source/WTF/wtf/cocoa/RuntimeApplicationChecksCocoa.h
index e841865..88eac10 100644
--- a/Source/WTF/wtf/cocoa/RuntimeApplicationChecksCocoa.h
+++ b/Source/WTF/wtf/cocoa/RuntimeApplicationChecksCocoa.h
@@ -94,6 +94,7 @@
     WebSQLDisabledByDefaultInLegacyWebKit,
     WKContentViewDoesNotOverrideKeyCommands,
     WKWebsiteDataStoreInitReturningNil,
+    UIBackForwardSkipsHistoryItemsWithoutUserGesture,
 
     NumberOfBehaviors
 };
diff --git a/Source/WebCore/history/HistoryItem.h b/Source/WebCore/history/HistoryItem.h
index 0a065d4..3173bff 100644
--- a/Source/WebCore/history/HistoryItem.h
+++ b/Source/WebCore/history/HistoryItem.h
@@ -209,6 +209,9 @@
     void setWasRestoredFromSession(bool wasRestoredFromSession) { m_wasRestoredFromSession = wasRestoredFromSession; }
     bool wasRestoredFromSession() const { return m_wasRestoredFromSession; }
 
+    void setWasCreatedByJSWithoutUserInteraction(bool wasCreatedByJSWithoutUserInteraction) { m_wasCreatedByJSWithoutUserInteraction = wasCreatedByJSWithoutUserInteraction; }
+    bool wasCreatedByJSWithoutUserInteraction() const { return m_wasCreatedByJSWithoutUserInteraction; }
+
 #if !LOG_DISABLED
     const char* logString() const;
 #endif
@@ -246,6 +249,7 @@
     bool m_lastVisitWasFailure { false };
     bool m_isTargetItem { false };
     bool m_wasRestoredFromSession { false };
+    bool m_wasCreatedByJSWithoutUserInteraction { false };
     bool m_shouldRestoreScrollPosition { true };
 
     // If two HistoryItems have the same item sequence number, then they are
diff --git a/Source/WebCore/loader/HistoryController.cpp b/Source/WebCore/loader/HistoryController.cpp
index 3cf08b9..50d39f0 100644
--- a/Source/WebCore/loader/HistoryController.cpp
+++ b/Source/WebCore/loader/HistoryController.cpp
@@ -859,6 +859,9 @@
     ASSERT(page);
 
     bool shouldRestoreScrollPosition = m_currentItem->shouldRestoreScrollPosition();
+
+    if (!UserGestureIndicator::processingUserGesture(m_frame.document()))
+        m_currentItem->setWasCreatedByJSWithoutUserInteraction(true);
     
     // Get a HistoryItem tree for the current frame tree.
     Ref<HistoryItem> topItem = m_frame.mainFrame().loader().history().createItemTree(m_frame, false);
diff --git a/Source/WebKit/Shared/SessionState.cpp b/Source/WebKit/Shared/SessionState.cpp
index 333c5b3..dad8a95 100644
--- a/Source/WebKit/Shared/SessionState.cpp
+++ b/Source/WebKit/Shared/SessionState.cpp
@@ -186,6 +186,7 @@
         encoder << sessionStateObject->wireBytes();
 
     encoder << shouldOpenExternalURLsPolicy;
+    encoder << wasCreatedByJSWithoutUserInteraction;
 }
 
 bool PageState::decode(IPC::Decoder& decoder, PageState& result)
@@ -216,6 +217,10 @@
         return false;
 
     result.shouldOpenExternalURLsPolicy = *shouldOpenExternalURLsPolicy;
+
+    if (!decoder.decode(result.wasCreatedByJSWithoutUserInteraction))
+        return false;
+
     return true;
 }
 
diff --git a/Source/WebKit/Shared/SessionState.h b/Source/WebKit/Shared/SessionState.h
index 3c1e8ad..2e13e22 100644
--- a/Source/WebKit/Shared/SessionState.h
+++ b/Source/WebKit/Shared/SessionState.h
@@ -136,6 +136,7 @@
     FrameState mainFrameState;
     WebCore::ShouldOpenExternalURLsPolicy shouldOpenExternalURLsPolicy { WebCore::ShouldOpenExternalURLsPolicy::ShouldNotAllow };
     RefPtr<WebCore::SerializedScriptValue> sessionStateObject;
+    bool wasCreatedByJSWithoutUserInteraction { false };
 };
 
 struct BackForwardListItemState {
diff --git a/Source/WebKit/Shared/WebBackForwardListItem.h b/Source/WebKit/Shared/WebBackForwardListItem.h
index 6642e20..f2e4a17 100644
--- a/Source/WebKit/Shared/WebBackForwardListItem.h
+++ b/Source/WebKit/Shared/WebBackForwardListItem.h
@@ -68,6 +68,7 @@
     const String& originalURL() const { return m_itemState.pageState.mainFrameState.originalURLString; }
     const String& url() const { return m_itemState.pageState.mainFrameState.urlString; }
     const String& title() const { return m_itemState.pageState.title; }
+    bool wasCreatedByJSWithoutUserInteraction() const { return m_itemState.pageState.wasCreatedByJSWithoutUserInteraction; }
 
     const URL& resourceDirectoryURL() const { return m_resourceDirectoryURL; }
     void setResourceDirectoryURL(URL&& url) { m_resourceDirectoryURL = WTFMove(url); }
diff --git a/Source/WebKit/UIProcess/WebPageProxy.cpp b/Source/WebKit/UIProcess/WebPageProxy.cpp
index b8c1355..274bb31 100644
--- a/Source/WebKit/UIProcess/WebPageProxy.cpp
+++ b/Source/WebKit/UIProcess/WebPageProxy.cpp
@@ -1824,9 +1824,35 @@
 #endif
 }
 
+enum class NavigationDirection { Backward, Forward };
+static WebBackForwardListItem* itemSkippingBackForwardItemsAddedByJSWithoutUserGesture(const WebBackForwardList& backForwardList, NavigationDirection direction)
+{
+    auto delta = direction == NavigationDirection::Backward ? -1 : 1;
+    int itemIndex = delta;
+    auto* item = backForwardList.itemAtIndex(itemIndex);
+    if (!item)
+        return nullptr;
+
+#if PLATFORM(COCOA)
+    if (!linkedOnOrAfterSDKWithBehavior(SDKAlignedBehavior::UIBackForwardSkipsHistoryItemsWithoutUserGesture))
+        return item;
+#endif
+
+    auto* originalItem = item;
+    while (item->wasCreatedByJSWithoutUserInteraction()) {
+        itemIndex += delta;
+        item = backForwardList.itemAtIndex(itemIndex);
+        if (!item)
+            return originalItem;
+        RELEASE_LOG(Loading, "UI Navigation is skipping a WebBackForwardListItem because it was added by JavaScript without user interaction");
+    }
+    return item;
+}
+
 RefPtr<API::Navigation> WebPageProxy::goForward()
 {
-    WebBackForwardListItem* forwardItem = m_backForwardList->forwardItem();
+    WEBPAGEPROXY_RELEASE_LOG(Loading, "goForward:");
+    auto* forwardItem = itemSkippingBackForwardItemsAddedByJSWithoutUserGesture(m_backForwardList, NavigationDirection::Forward);
     if (!forwardItem)
         return nullptr;
 
@@ -1835,7 +1861,8 @@
 
 RefPtr<API::Navigation> WebPageProxy::goBack()
 {
-    WebBackForwardListItem* backItem = m_backForwardList->backItem();
+    WEBPAGEPROXY_RELEASE_LOG(Loading, "goBack:");
+    auto* backItem = itemSkippingBackForwardItemsAddedByJSWithoutUserGesture(m_backForwardList, NavigationDirection::Backward);
     if (!backItem)
         return nullptr;
 
diff --git a/Source/WebKit/WebProcess/WebCoreSupport/SessionStateConversion.cpp b/Source/WebKit/WebProcess/WebCoreSupport/SessionStateConversion.cpp
index 42b4e00..371715a 100644
--- a/Source/WebKit/WebProcess/WebCoreSupport/SessionStateConversion.cpp
+++ b/Source/WebKit/WebProcess/WebCoreSupport/SessionStateConversion.cpp
@@ -114,6 +114,7 @@
     state.pageState.mainFrameState = toFrameState(historyItem);
     state.pageState.shouldOpenExternalURLsPolicy = historyItem.shouldOpenExternalURLsPolicy();
     state.pageState.sessionStateObject = historyItem.stateObject();
+    state.pageState.wasCreatedByJSWithoutUserInteraction = historyItem.wasCreatedByJSWithoutUserInteraction();
     state.hasCachedPage = historyItem.isInBackForwardCache();
     return state;
 }
diff --git a/Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj b/Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj
index 1636721..fa0aa55 100644
--- a/Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj
+++ b/Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj
@@ -244,6 +244,7 @@
 		46C519E61D3563FD00DAA51A /* LocalStorageNullEntries.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = 46C519E21D35629600DAA51A /* LocalStorageNullEntries.html */; };
 		46C519E71D3563FD00DAA51A /* LocalStorageNullEntries.localstorage in Copy Resources */ = {isa = PBXBuildFile; fileRef = 46C519E31D35629600DAA51A /* LocalStorageNullEntries.localstorage */; };
 		46C519E81D3563FD00DAA51A /* LocalStorageNullEntries.localstorage-shm in Copy Resources */ = {isa = PBXBuildFile; fileRef = 46C519E41D35629600DAA51A /* LocalStorageNullEntries.localstorage-shm */; };
+		46E45EE62863D3E200441B14 /* WKBackForwardList.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1F83571A1D3FFB0E00E3967B /* WKBackForwardList.mm */; };
 		46E816F81E79E29C00375ADC /* RestoreStateAfterTermination.mm in Sources */ = {isa = PBXBuildFile; fileRef = 46E816F71E79E29100375ADC /* RestoreStateAfterTermination.mm */; };
 		46F03C1C255B2D5A00AA51C5 /* audio-context-playing.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = 46F03C1B255B2D3600AA51C5 /* audio-context-playing.html */; };
 		46FA2FEE23846CA5000CCB0C /* HTTPHeaderMap.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 46FA2FED23846C9A000CCB0C /* HTTPHeaderMap.cpp */; };
@@ -6040,6 +6041,7 @@
 				7CCE7F1C1A411AE600447C4C /* WillSendSubmitEvent.cpp in Sources */,
 				7CCE7ED81A411A7E00447C4C /* WillSendSubmitEvent.mm in Sources */,
 				7CCE7ED91A411A7E00447C4C /* WindowlessWebViewWithMedia.mm in Sources */,
+				46E45EE62863D3E200441B14 /* WKBackForwardList.mm in Sources */,
 				7CCE7F2E1A411B1000447C4C /* WKBrowsingContextGroupTest.mm in Sources */,
 				7CCE7F2F1A411B1000447C4C /* WKBrowsingContextLoadDelegateTest.mm in Sources */,
 				7CCE7F1D1A411AE600447C4C /* WKImageCreateCGImageCrash.cpp in Sources */,
diff --git a/Tools/TestWebKitAPI/Tests/WebKit/WKBackForwardList.mm b/Tools/TestWebKitAPI/Tests/WebKit/WKBackForwardList.mm
index 07dc665..ae64c6c 100644
--- a/Tools/TestWebKitAPI/Tests/WebKit/WKBackForwardList.mm
+++ b/Tools/TestWebKitAPI/Tests/WebKit/WKBackForwardList.mm
@@ -33,6 +33,7 @@
 #import <WebKit/WKWebViewPrivate.h>
 #import <WebKit/_WKSessionState.h>
 #import <wtf/RetainPtr.h>
+#import <wtf/text/WTFString.h>
 
 static NSString *loadableURL1 = @"data:text/html,no%20error%20A";
 static NSString *loadableURL2 = @"data:text/html,no%20error%20B";
@@ -309,3 +310,107 @@
     EXPECT_STREQ([[list.currentItem URL] absoluteString].UTF8String, url3.absoluteString.UTF8String);
 }
 
+TEST(WKBackForwardList, BackForwardNavigationSkipsItemsWithoutUserGesture)
+{
+    auto webView = adoptNS([[WKWebView alloc] init]);
+    NSURL *url1 = [[NSBundle mainBundle] URLForResource:@"simple" withExtension:@"html" subdirectory:@"TestWebKitAPI.resources"];
+    NSURL *url2 = [[NSBundle mainBundle] URLForResource:@"simple2" withExtension:@"html" subdirectory:@"TestWebKitAPI.resources"];
+    [webView loadRequest:[NSURLRequest requestWithURL:url1]];
+    [webView _test_waitForDidFinishNavigation];
+
+    [webView loadRequest:[NSURLRequest requestWithURL:url2]];
+    [webView _test_waitForDidFinishNavigation];
+
+    // Add back/forward list items without user gestures.
+    done = false;
+    [webView _evaluateJavaScriptWithoutUserGesture:@"history.pushState(null, document.title, location.pathname + '#a');" completionHandler:^(id, NSError *) {
+        done = true;
+    }];
+    TestWebKitAPI::Util::run(&done);
+    done = false;
+    [webView _evaluateJavaScriptWithoutUserGesture:@"history.pushState(null, document.title, location.pathname + '#b');" completionHandler:^(id, NSError *) {
+        done = true;
+    }];
+    TestWebKitAPI::Util::run(&done);
+    done = false;
+    [webView _evaluateJavaScriptWithoutUserGesture:@"history.pushState(null, document.title, location.pathname + '#c');" completionHandler:^(id, NSError *) {
+        done = true;
+    }];
+    TestWebKitAPI::Util::run(&done);
+
+    EXPECT_EQ([webView backForwardList].backList.count, 4U);
+    EXPECT_EQ([webView backForwardList].forwardList.count, 0U);
+
+    auto* lastURL = [webView URL];
+
+    // Going back should skip the back/forward list items without user gestures.
+    [webView goBack];
+    [webView _test_waitForDidFinishNavigationOrSameDocumentNavigation];
+
+    EXPECT_STREQ([webView URL].absoluteString.UTF8String, url1.absoluteString.UTF8String);
+
+    EXPECT_EQ([webView backForwardList].backList.count, 0U);
+    EXPECT_EQ([webView backForwardList].forwardList.count, 4U);
+
+    // Going forward should skip the back/forward list items without user gestures.
+    [webView goForward];
+    [webView _test_waitForDidFinishNavigationOrSameDocumentNavigation];
+
+    EXPECT_STREQ([webView URL].absoluteString.UTF8String, lastURL.absoluteString.UTF8String);
+
+    EXPECT_EQ([webView backForwardList].backList.count, 4U);
+    EXPECT_EQ([webView backForwardList].forwardList.count, 0U);
+
+    NSString *currentURLString = [webView URL].absoluteString;
+    NSString *expectedURLString = makeString(String([[NSBundle mainBundle] URLForResource:@"simple2" withExtension:@"html" subdirectory:@"TestWebKitAPI.resources"].absoluteString), "#c");
+    EXPECT_WK_STREQ(currentURLString, expectedURLString);
+
+    // Navigating via the JS API shouldn't skip those back/forward list items.
+    [webView _evaluateJavaScriptWithoutUserGesture:@"history.back();" completionHandler:^(id, NSError *) { }];
+    [webView _test_waitForDidFinishNavigationOrSameDocumentNavigation];
+
+    expectedURLString = makeString(String([[NSBundle mainBundle] URLForResource:@"simple2" withExtension:@"html" subdirectory:@"TestWebKitAPI.resources"].absoluteString), "#b");
+    EXPECT_WK_STREQ([webView URL].absoluteString.UTF8String, expectedURLString.UTF8String);
+}
+
+TEST(WKBackForwardList, BackForwardNavigationDoesNotSkipItemsWithUserGesture)
+{
+    auto webView = adoptNS([[WKWebView alloc] init]);
+    NSURL *url1 = [[NSBundle mainBundle] URLForResource:@"simple" withExtension:@"html" subdirectory:@"TestWebKitAPI.resources"];
+    NSURL *url2 = [[NSBundle mainBundle] URLForResource:@"simple2" withExtension:@"html" subdirectory:@"TestWebKitAPI.resources"];
+    [webView loadRequest:[NSURLRequest requestWithURL:url1]];
+    [webView _test_waitForDidFinishNavigation];
+
+    [webView loadRequest:[NSURLRequest requestWithURL:url2]];
+    [webView _test_waitForDidFinishNavigation];
+
+    // Add back/forward list items without user gestures.
+    done = false;
+    [webView evaluateJavaScript:@"history.pushState(null, document.title, location.pathname + '#a');" completionHandler:^(id, NSError *) {
+        done = true;
+    }];
+    TestWebKitAPI::Util::run(&done);
+
+    auto* lastURL = [webView URL];
+    EXPECT_FALSE([lastURL isEqual:url2]);
+
+    [webView goBack];
+    [webView _test_waitForDidFinishNavigationOrSameDocumentNavigation];
+
+    EXPECT_STREQ([webView URL].absoluteString.UTF8String, url2.absoluteString.UTF8String);
+
+    [webView goBack];
+    [webView _test_waitForDidFinishNavigationOrSameDocumentNavigation];
+
+    EXPECT_STREQ([webView URL].absoluteString.UTF8String, url1.absoluteString.UTF8String);
+
+    [webView goForward];
+    [webView _test_waitForDidFinishNavigationOrSameDocumentNavigation];
+
+    EXPECT_STREQ([webView URL].absoluteString.UTF8String, url2.absoluteString.UTF8String);
+
+    [webView goForward];
+    [webView _test_waitForDidFinishNavigationOrSameDocumentNavigation];
+
+    EXPECT_STREQ([webView URL].absoluteString.UTF8String, lastURL.absoluteString.UTF8String);
+}
diff --git a/Tools/TestWebKitAPI/cocoa/TestNavigationDelegate.h b/Tools/TestWebKitAPI/cocoa/TestNavigationDelegate.h
index b727dbb..e62b26c 100644
--- a/Tools/TestWebKitAPI/cocoa/TestNavigationDelegate.h
+++ b/Tools/TestWebKitAPI/cocoa/TestNavigationDelegate.h
@@ -37,6 +37,7 @@
 @property (nonatomic, copy) void (^didStartProvisionalNavigation)(WKWebView *, WKNavigation *);
 @property (nonatomic, copy) void (^didCommitNavigation)(WKWebView *, WKNavigation *);
 @property (nonatomic, copy) void (^didFinishNavigation)(WKWebView *, WKNavigation *);
+@property (nonatomic, copy) void (^didSameDocumentNavigation)(WKWebView *, WKNavigation *);
 @property (nonatomic, copy) void (^renderingProgressDidChange)(WKWebView *, _WKRenderingProgressEvents);
 @property (nonatomic, copy) void (^webContentProcessDidTerminate)(WKWebView *);
 @property (nonatomic, copy) void (^didReceiveAuthenticationChallenge)(WKWebView *, NSURLAuthenticationChallenge *, void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *));
@@ -44,6 +45,7 @@
 
 - (void)waitForDidStartProvisionalNavigation;
 - (void)waitForDidFinishNavigation;
+- (void)waitForDidFinishNavigationOrSameDocumentNavigation;
 - (void)waitForDidFinishNavigationWithPreferences:(WKWebpagePreferences *)preferences;
 - (NSError *)waitForDidFailProvisionalNavigation;
 
@@ -52,6 +54,7 @@
 @interface WKWebView (TestWebKitAPIExtras)
 - (void)_test_waitForDidStartProvisionalNavigation;
 - (void)_test_waitForDidFinishNavigation;
+- (void)_test_waitForDidFinishNavigationOrSameDocumentNavigation;
 - (void)_test_waitForDidFinishNavigationWithPreferences:(WKWebpagePreferences *)preferences;
 - (void)_test_waitForDidFinishNavigationWithoutPresentationUpdate;
 - (void)_test_waitForDidFailProvisionalNavigation;
diff --git a/Tools/TestWebKitAPI/cocoa/TestNavigationDelegate.mm b/Tools/TestWebKitAPI/cocoa/TestNavigationDelegate.mm
index fa67443..76e6a80 100644
--- a/Tools/TestWebKitAPI/cocoa/TestNavigationDelegate.mm
+++ b/Tools/TestWebKitAPI/cocoa/TestNavigationDelegate.mm
@@ -98,6 +98,12 @@
         completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
 }
 
+- (void)_webView:(WKWebView *)webView navigation:(WKNavigation *)navigation didSameDocumentNavigation:(_WKSameDocumentNavigationType)navigationType
+{
+    if (_didSameDocumentNavigation)
+        _didSameDocumentNavigation(webView, navigation);
+}
+
 - (void)waitForDidStartProvisionalNavigation
 {
     EXPECT_FALSE(self.didStartProvisionalNavigation);
@@ -126,6 +132,23 @@
     self.didFinishNavigation = nil;
 }
 
+- (void)waitForDidFinishNavigationOrSameDocumentNavigation
+{
+    EXPECT_FALSE(self.didFinishNavigation);
+
+    __block bool finished = false;
+    self.didFinishNavigation = ^(WKWebView *, WKNavigation *) {
+        finished = true;
+    };
+    self.didSameDocumentNavigation = ^(WKWebView *, WKNavigation *) {
+        finished = true;
+    };
+
+    TestWebKitAPI::Util::run(&finished);
+
+    self.didFinishNavigation = nil;
+}
+
 - (void)waitForWebContentProcessDidTerminate
 {
     EXPECT_FALSE(self.webContentProcessDidTerminate);
@@ -242,6 +265,17 @@
 #endif
 }
 
+- (void)_test_waitForDidFinishNavigationOrSameDocumentNavigation
+{
+    EXPECT_FALSE(self.navigationDelegate);
+
+    auto navigationDelegate = adoptNS([[TestNavigationDelegate alloc] init]);
+    self.navigationDelegate = navigationDelegate.get();
+    [navigationDelegate waitForDidFinishNavigationOrSameDocumentNavigation];
+
+    self.navigationDelegate = nil;
+}
+
 - (void)_test_waitForWebContentProcessDidTerminate
 {
     EXPECT_FALSE(self.navigationDelegate);