| // Copyright 2012 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "content/public/test/test_navigation_observer.h" |
| |
| #include "base/functional/bind.h" |
| #include "base/memory/raw_ptr.h" |
| #include "content/browser/renderer_host/navigation_request.h" |
| #include "content/browser/web_contents/web_contents_impl.h" |
| #include "content/public/browser/browser_url_handler.h" |
| #include "content/public/browser/web_contents_observer.h" |
| #include "content/public/test/browser_test_utils.h" |
| |
| namespace content { |
| |
| class TestNavigationObserver::TestWebContentsObserver |
| : public WebContentsObserver { |
| public: |
| TestWebContentsObserver(TestNavigationObserver* parent, |
| WebContents* web_contents) |
| : WebContentsObserver(web_contents), |
| parent_(parent) { |
| } |
| |
| TestWebContentsObserver(const TestWebContentsObserver&) = delete; |
| TestWebContentsObserver& operator=(const TestWebContentsObserver&) = delete; |
| |
| private: |
| // WebContentsObserver: |
| void NavigationEntryCommitted( |
| const LoadCommittedDetails& load_details) override { |
| parent_->OnNavigationEntryCommitted(this, web_contents(), load_details); |
| } |
| |
| void WebContentsDestroyed() override { |
| parent_->OnWebContentsDestroyed(this, web_contents()); |
| } |
| |
| void DidStartLoading() override { |
| parent_->OnDidStartLoading(web_contents()); |
| } |
| |
| void DidStopLoading() override { |
| parent_->OnDidStopLoading(web_contents()); |
| } |
| |
| void DidStartNavigation(NavigationHandle* navigation_handle) override { |
| if (navigation_handle->IsSameDocument()) |
| return; |
| |
| parent_->OnDidStartNavigation(navigation_handle); |
| } |
| |
| void DidFinishNavigation(NavigationHandle* navigation_handle) override { |
| parent_->OnDidFinishNavigation(navigation_handle); |
| } |
| |
| raw_ptr<TestNavigationObserver> parent_; |
| }; |
| |
| TestNavigationObserver::WebContentsState::WebContentsState() = default; |
| TestNavigationObserver::WebContentsState::WebContentsState( |
| WebContentsState&& other) = default; |
| TestNavigationObserver::WebContentsState& |
| TestNavigationObserver::WebContentsState::operator=(WebContentsState&& other) = |
| default; |
| TestNavigationObserver::WebContentsState::~WebContentsState() = default; |
| |
| TestNavigationObserver::TestNavigationObserver( |
| WebContents* web_contents, |
| int expected_number_of_navigations, |
| MessageLoopRunner::QuitMode quit_mode, |
| bool ignore_uncommitted_navigations) |
| : TestNavigationObserver(web_contents, |
| expected_number_of_navigations, |
| absl::nullopt /* target_url */, |
| absl::nullopt /* target_error */, |
| quit_mode, |
| ignore_uncommitted_navigations) {} |
| |
| TestNavigationObserver::TestNavigationObserver( |
| WebContents* web_contents, |
| MessageLoopRunner::QuitMode quit_mode, |
| bool ignore_uncommitted_navigations) |
| : TestNavigationObserver(web_contents, |
| 1, |
| quit_mode, |
| ignore_uncommitted_navigations) {} |
| |
| TestNavigationObserver::TestNavigationObserver( |
| WebContents* web_contents, |
| net::Error expected_target_error, |
| MessageLoopRunner::QuitMode quit_mode, |
| bool ignore_uncommitted_navigations) |
| : TestNavigationObserver(web_contents, |
| 1 /* num_of_navigations */, |
| absl::nullopt, |
| expected_target_error, |
| quit_mode, |
| ignore_uncommitted_navigations) {} |
| |
| TestNavigationObserver::TestNavigationObserver( |
| const GURL& expected_target_url, |
| MessageLoopRunner::QuitMode quit_mode, |
| bool ignore_uncommitted_navigations) |
| : TestNavigationObserver(nullptr, |
| 1 /* num_of_navigations */, |
| expected_target_url, |
| absl::nullopt /* target_error */, |
| quit_mode, |
| ignore_uncommitted_navigations) {} |
| |
| TestNavigationObserver::~TestNavigationObserver() = default; |
| |
| void TestNavigationObserver::Wait() { |
| was_event_consumed_ = false; |
| TRACE_EVENT1("test", "TestNavigationObserver::Wait", "params", |
| [&](perfetto::TracedValue ctx) { |
| // TODO(crbug.com/1183371): Replace this with passing more |
| // parameters to TRACE_EVENT directly when available. |
| auto dict = std::move(ctx).WriteDictionary(); |
| dict.Add("wait_event", wait_event_); |
| dict.Add("ignore_uncommitted_navigations", |
| ignore_uncommitted_navigations_); |
| dict.Add("expected_target_url", expected_target_url_); |
| dict.Add("expected_initial_url", expected_initial_url_); |
| dict.Add("expected_target_error", expected_target_error_); |
| }); |
| message_loop_runner_->Run(); |
| } |
| |
| void TestNavigationObserver::WaitForNavigationFinished() { |
| wait_event_ = WaitEvent::kNavigationFinished; |
| Wait(); |
| } |
| |
| void TestNavigationObserver::StartWatchingNewWebContents() { |
| creation_subscription_ = RegisterWebContentsCreationCallback( |
| base::BindRepeating(&TestNavigationObserver::OnWebContentsCreated, |
| base::Unretained(this))); |
| } |
| |
| void TestNavigationObserver::StopWatchingNewWebContents() { |
| creation_subscription_ = base::CallbackListSubscription(); |
| } |
| |
| void TestNavigationObserver::WatchExistingWebContents() { |
| for (auto* web_contents : WebContentsImpl::GetAllWebContents()) |
| RegisterAsObserver(web_contents); |
| } |
| |
| void TestNavigationObserver::RegisterAsObserver(WebContents* web_contents) { |
| web_contents_state_[web_contents].observer = |
| std::make_unique<TestWebContentsObserver>(this, web_contents); |
| } |
| |
| TestNavigationObserver::TestNavigationObserver( |
| WebContents* web_contents, |
| int expected_number_of_navigations, |
| const absl::optional<GURL>& expected_target_url, |
| absl::optional<net::Error> expected_target_error, |
| MessageLoopRunner::QuitMode quit_mode, |
| bool ignore_uncommitted_navigations) |
| : wait_event_(WaitEvent::kLoadStopped), |
| navigations_completed_(0), |
| expected_number_of_navigations_(expected_number_of_navigations), |
| expected_target_url_(expected_target_url), |
| expected_initial_url_(absl::nullopt), |
| expected_target_error_(expected_target_error), |
| ignore_uncommitted_navigations_(ignore_uncommitted_navigations), |
| last_navigation_succeeded_(false), |
| last_net_error_code_(net::OK), |
| message_loop_runner_(new MessageLoopRunner(quit_mode)) { |
| if (web_contents) |
| RegisterAsObserver(web_contents); |
| } |
| |
| void TestNavigationObserver::OnWebContentsCreated(WebContents* web_contents) { |
| RegisterAsObserver(web_contents); |
| } |
| |
| void TestNavigationObserver::OnWebContentsDestroyed( |
| TestWebContentsObserver* observer, |
| WebContents* web_contents) { |
| auto web_contents_state_iter = web_contents_state_.find(web_contents); |
| DCHECK(web_contents_state_iter != web_contents_state_.end()); |
| DCHECK_EQ(web_contents_state_iter->second.observer.get(), observer); |
| |
| web_contents_state_.erase(web_contents_state_iter); |
| } |
| |
| void TestNavigationObserver::OnNavigationEntryCommitted( |
| TestWebContentsObserver* observer, |
| WebContents* web_contents, |
| const LoadCommittedDetails& load_details) { |
| WebContentsState* web_contents_state = GetWebContentsState(web_contents); |
| web_contents_state->navigation_started = true; |
| } |
| |
| void TestNavigationObserver::OnDidStartLoading(WebContents* web_contents) { |
| WebContentsState* web_contents_state = GetWebContentsState(web_contents); |
| web_contents_state->navigation_started = true; |
| } |
| |
| void TestNavigationObserver::OnDidStopLoading(WebContents* web_contents) { |
| WebContentsState* web_contents_state = GetWebContentsState(web_contents); |
| if (!web_contents_state->navigation_started) |
| return; |
| |
| if (wait_event_ == WaitEvent::kLoadStopped) |
| EventTriggered(web_contents_state); |
| } |
| |
| void TestNavigationObserver::OnDidStartNavigation( |
| NavigationHandle* navigation_handle) { |
| if (expected_target_url_.has_value() && |
| expected_target_url_.value() != navigation_handle->GetURL()) { |
| return; |
| } |
| if (!DoesNavigationMatchExpectedInitialUrl( |
| NavigationRequest::From(navigation_handle))) { |
| return; |
| } |
| |
| WebContentsState* web_contents_state = |
| GetWebContentsState(navigation_handle->GetWebContents()); |
| if (!web_contents_state->navigation_started) |
| return; |
| |
| last_navigation_succeeded_ = false; |
| } |
| |
| void TestNavigationObserver::OnDidFinishNavigation( |
| NavigationHandle* navigation_handle) { |
| if (ignore_uncommitted_navigations_ && !navigation_handle->HasCommitted()) |
| return; |
| |
| NavigationRequest* request = NavigationRequest::From(navigation_handle); |
| if (expected_target_url_.has_value() && |
| expected_target_url_.value() != navigation_handle->GetURL()) { |
| return; |
| } |
| if (!DoesNavigationMatchExpectedInitialUrl(request)) |
| return; |
| if (expected_target_error_.has_value() && |
| expected_target_error_.value() != navigation_handle->GetNetErrorCode()) { |
| return; |
| } |
| |
| WebContentsState* web_contents_state = |
| GetWebContentsState(navigation_handle->GetWebContents()); |
| |
| // TODO(crbug.com/1233764): It is generally the case that we've received load |
| // started events by this point, but we don't send load events for prerendered |
| // pages (by design). It's also the case that frame tree nodes don't report |
| // load start if the tree is already loading. For all of prerendering, |
| // subframes and fenced frames (i.e., the cases where we cannot rely on |
| // navigation_started being set correctly), we're not in the primary main |
| // frame, so the DCHECK has been updated to ignore these cases. We also only |
| // enforce this check if we haven't already called EventTriggered (since this |
| // will reset navigation_started and can cause errors in subsequent |
| // DidFinishNavigation calls). All this being said, we should, in general, |
| // move away from NotificationService and related events. |
| DCHECK(was_event_consumed_ || !navigation_handle->IsInPrimaryMainFrame() || |
| web_contents_state->navigation_started); |
| |
| if (HasFilter()) |
| web_contents_state->last_navigation_matches_filter = true; |
| |
| last_navigation_url_ = navigation_handle->GetURL(); |
| last_navigation_initiator_origin_ = request->common_params().initiator_origin; |
| last_initiator_frame_token_ = navigation_handle->GetInitiatorFrameToken(); |
| last_initiator_process_id_ = navigation_handle->GetInitiatorProcessId(); |
| last_navigation_succeeded_ = |
| navigation_handle->HasCommitted() && !navigation_handle->IsErrorPage(); |
| last_navigation_initiator_activation_and_ad_status_ = |
| navigation_handle->GetNavigationInitiatorActivationAndAdStatus(); |
| last_net_error_code_ = navigation_handle->GetNetErrorCode(); |
| last_nav_entry_id_ = |
| NavigationRequest::From(navigation_handle)->nav_entry_id(); |
| last_source_site_instance_ = navigation_handle->GetSourceSiteInstance(); |
| next_page_ukm_source_id_ = navigation_handle->GetNextPageUkmSourceId(); |
| |
| // Allow extending classes to fetch data available via navigation_handle. |
| NavigationOfInterestDidFinish(navigation_handle); |
| |
| if (wait_event_ == WaitEvent::kNavigationFinished) |
| EventTriggered(web_contents_state); |
| } |
| |
| void TestNavigationObserver::NavigationOfInterestDidFinish(NavigationHandle*) { |
| // Nothing in the base class. |
| } |
| |
| void TestNavigationObserver::EventTriggered( |
| WebContentsState* web_contents_state) { |
| if (HasFilter() && !web_contents_state->last_navigation_matches_filter) |
| return; |
| |
| DCHECK_GE(navigations_completed_, 0); |
| ++navigations_completed_; |
| if (navigations_completed_ != expected_number_of_navigations_) { |
| return; |
| } |
| |
| was_event_consumed_ = true; |
| web_contents_state->navigation_started = false; |
| message_loop_runner_->Quit(); |
| } |
| |
| bool TestNavigationObserver::DoesNavigationMatchExpectedInitialUrl( |
| NavigationRequest* navigation_request) { |
| if (!expected_initial_url_.has_value()) |
| return true; |
| |
| // Find the real URL being navigated to (e.g. stripping the "view-source:" |
| // prefix if necessary). |
| GURL expected_url = *expected_initial_url_; |
| BrowserContext* browser_context = navigation_request->frame_tree_node() |
| ->navigator() |
| .controller() |
| .GetBrowserContext(); |
| BrowserURLHandler::GetInstance()->RewriteURLIfNecessary(&expected_url, |
| browser_context); |
| |
| // Debug URLs do not go through NavigationRequest and therefore cannot be used |
| // as an `expected_url`. |
| DCHECK(!blink::IsRendererDebugURL(expected_url)); |
| |
| GURL actual_url = navigation_request->GetOriginalRequestURL(); |
| return actual_url == expected_url; |
| } |
| |
| bool TestNavigationObserver::HasFilter() { |
| return expected_target_url_.has_value() || |
| expected_initial_url_.has_value() || |
| expected_target_error_.has_value(); |
| } |
| |
| TestNavigationObserver::WebContentsState* |
| TestNavigationObserver::GetWebContentsState(WebContents* web_contents) { |
| auto web_contents_state_iter = web_contents_state_.find(web_contents); |
| DCHECK(web_contents_state_iter != web_contents_state_.end()); |
| return &(web_contents_state_iter->second); |
| } |
| |
| } // namespace content |