| // Copyright 2021 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/prerender_test_util.h" |
| |
| #include <tuple> |
| |
| #include "base/functional/callback_helpers.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/test/bind.h" |
| #include "base/trace_event/typed_macros.h" |
| #include "base/types/cxx23_to_underlying.h" |
| #include "content/browser/preloading/prerender/prerender_features.h" |
| #include "content/browser/preloading/prerender/prerender_final_status.h" |
| #include "content/browser/preloading/prerender/prerender_handle_impl.h" |
| #include "content/browser/preloading/prerender/prerender_host.h" |
| #include "content/browser/preloading/prerender/prerender_host_registry.h" |
| #include "content/browser/renderer_host/frame_tree.h" |
| #include "content/browser/renderer_host/render_frame_host_impl.h" |
| #include "content/browser/web_contents/web_contents_impl.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/common/isolated_world_ids.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "content/public/test/test_navigation_observer.h" |
| #include "net/test/embedded_test_server/embedded_test_server.h" |
| #include "third_party/blink/public/common/features.h" |
| |
| namespace content { |
| namespace test { |
| namespace { |
| |
| constexpr char kAddSpeculationRuleScript[] = R"({ |
| const script = document.createElement('script'); |
| script.type = 'speculationrules'; |
| script.text = `{ |
| "prerender": [{ $1 }] |
| }`; |
| document.head.appendChild(script); |
| })"; |
| |
| constexpr char kAddSpeculationRuleWithRulesetTagScript[] = R"({ |
| const script = document.createElement('script'); |
| script.type = 'speculationrules'; |
| script.text = `{ |
| "tag": "$1", |
| "prerender": [{ $2 }] |
| }`; |
| document.head.appendChild(script); |
| })"; |
| |
| std::string ConvertEagernessToString( |
| blink::mojom::SpeculationEagerness eagerness) { |
| switch (eagerness) { |
| case blink::mojom::SpeculationEagerness::kImmediate: |
| return "immediate"; |
| case blink::mojom::SpeculationEagerness::kEager: |
| return "eager"; |
| case blink::mojom::SpeculationEagerness::kModerate: |
| return "moderate"; |
| case blink::mojom::SpeculationEagerness::kConservative: |
| return "conservative"; |
| } |
| } |
| |
| // Builds <script type="speculationrules"> element for prerendering. |
| std::string BuildScriptElementSpeculationRules( |
| const std::vector<GURL>& prerendering_urls, |
| std::optional<blink::mojom::SpeculationEagerness> eagerness, |
| std::optional<std::string> no_vary_search_hint, |
| const std::string& target_hint, |
| std::optional<std::string> ruleset_tag) { |
| std::stringstream ss; |
| |
| // Add source filed. |
| ss << R"("source": "list", )"; |
| |
| // Concatenate the given URLs with a comma separator. |
| std::stringstream urls_ss; |
| for (size_t i = 0; i < prerendering_urls.size(); i++) { |
| // Wrap the url with double quotes. |
| urls_ss << base::StringPrintf(R"("%s")", |
| prerendering_urls[i].spec().c_str()); |
| if (i + 1 < prerendering_urls.size()) { |
| urls_ss << ", "; |
| } |
| } |
| // Add urls fields. |
| ss << base::StringPrintf(R"("urls": [ %s ])", urls_ss.str().c_str()); |
| |
| // Add eagerness field. |
| if (eagerness.has_value()) { |
| ss << base::StringPrintf( |
| R"(, "eagerness": "%s")", |
| ConvertEagernessToString(eagerness.value()).c_str()); |
| } |
| if (no_vary_search_hint.has_value()) { |
| ss << base::StringPrintf(R"(, "expects_no_vary_search": "%s")", |
| no_vary_search_hint.value().c_str()); |
| } |
| |
| // Add target_hint field. |
| if (!target_hint.empty()) { |
| ss << base::StringPrintf(R"(, "target_hint": "%s")", target_hint.c_str()); |
| } |
| |
| return ruleset_tag.has_value() |
| ? base::ReplaceStringPlaceholders( |
| kAddSpeculationRuleWithRulesetTagScript, |
| {ruleset_tag.value(), ss.str()}, nullptr) |
| : base::ReplaceStringPlaceholders(kAddSpeculationRuleScript, |
| {ss.str()}, nullptr); |
| } |
| |
| // TODO(crbug.com/428500219): Move these patterns to preloading_test_util.cc, |
| // and merge them to BuildScriptElementSpeculationRules. |
| constexpr char kAddSpeculationRulePrerenderUntilScriptScript[] = R"({ |
| const script = document.createElement('script'); |
| script.type = 'speculationrules'; |
| script.text = `{ |
| "prerender_until_script": [{ |
| "source": "list", |
| "urls": [$1], |
| "eagerness": $2 |
| }] |
| }`; |
| document.head.appendChild(script); |
| })"; |
| |
| constexpr char kAddSpeculationRulePrefetchScript[] = R"({ |
| const script = document.createElement('script'); |
| script.type = 'speculationrules'; |
| script.text = `{ |
| "prefetch": [{ |
| "source": "list", |
| "urls": [$1] |
| }] |
| }`; |
| document.head.appendChild(script); |
| })"; |
| |
| PrerenderHostRegistry& GetPrerenderHostRegistry(WebContents* web_contents) { |
| EXPECT_TRUE(content::BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| return *static_cast<WebContentsImpl*>(web_contents) |
| ->GetPrerenderHostRegistry(); |
| } |
| |
| PrerenderHost* GetPrerenderHostById(WebContents* web_contents, |
| FrameTreeNodeId host_id) { |
| auto& registry = GetPrerenderHostRegistry(web_contents); |
| return registry.FindNonReservedHostById(host_id); |
| } |
| |
| PrerenderHost* GetPrerenderHostByUrl(WebContents* web_contents, |
| const GURL& url) { |
| auto& registry = GetPrerenderHostRegistry(web_contents); |
| return registry.FindHostByUrlForTesting(url); |
| } |
| |
| PrerenderHost::LoadingOutcome WaitForPrerenderLoadingOutcome( |
| WebContents& web_contents, |
| const GURL& url) { |
| TRACE_EVENT("test", "PrerenderTestHelper::WaitForPrerenderLoadingOutcome", |
| "web_contents", web_contents, "url", url); |
| PrerenderHostRegistry& registry = GetPrerenderHostRegistry(&web_contents); |
| PrerenderHost* host = registry.FindHostByUrlForTesting(url); |
| // Wait for the host to be created if it hasn't yet. |
| if (!host) { |
| PrerenderHostRegistryObserver observer(web_contents); |
| observer.WaitForTrigger(url); |
| host = registry.FindHostByUrlForTesting(url); |
| CHECK_NE(host, nullptr); |
| } |
| return host->WaitForLoadStopForTesting(); |
| } |
| |
| } // namespace |
| |
| class PrerenderHostRegistryObserverImpl |
| : public PrerenderHostRegistry::Observer { |
| public: |
| explicit PrerenderHostRegistryObserverImpl(WebContents& web_contents) { |
| observation_.Observe(&GetPrerenderHostRegistry(&web_contents)); |
| } |
| |
| void WaitForTrigger(const GURL& url) { |
| ASSERT_FALSE(waiting_.contains(url)); |
| if (triggered_.contains(url)) { |
| return; |
| } |
| base::RunLoop loop; |
| waiting_[url] = loop.QuitClosure(); |
| loop.Run(); |
| } |
| |
| GURL WaitForNextTrigger() { |
| EXPECT_FALSE(waiting_next_); |
| GURL triggered_url; |
| base::RunLoop loop; |
| waiting_next_ = |
| base::BindLambdaForTesting([&triggered_url, &loop](const GURL& url) { |
| triggered_url = url; |
| loop.Quit(); |
| }); |
| loop.Run(); |
| return triggered_url; |
| } |
| |
| void NotifyOnTrigger(const GURL& url, base::OnceClosure callback) { |
| ASSERT_FALSE(waiting_.contains(url)); |
| if (triggered_.contains(url)) { |
| std::move(callback).Run(); |
| return; |
| } |
| waiting_[url] = std::move(callback); |
| } |
| |
| base::flat_set<GURL> GetTriggeredUrls() const { return triggered_; } |
| |
| void OnTrigger(const GURL& url) override { |
| if (triggered_.contains(url)) { |
| ASSERT_FALSE(waiting_.contains(url)); |
| return; |
| } |
| triggered_.insert(url); |
| |
| if (waiting_next_) { |
| std::move(waiting_next_).Run(url); |
| } |
| |
| auto iter = waiting_.find(url); |
| if (iter != waiting_.end()) { |
| auto callback = std::move(iter->second); |
| waiting_.erase(iter); |
| std::move(callback).Run(); |
| } |
| } |
| |
| void OnRegistryDestroyed() override { |
| EXPECT_TRUE(waiting_.empty()); |
| observation_.Reset(); |
| } |
| |
| base::ScopedObservation<PrerenderHostRegistry, |
| PrerenderHostRegistry::Observer> |
| observation_{this}; |
| |
| base::flat_map<GURL, base::OnceClosure> waiting_; |
| base::OnceCallback<void(const GURL&)> waiting_next_; |
| |
| // Set when prerendering is triggered. Doesn't yet support the case where |
| // prerendering is triggered, canceled, and then re-triggered for the same |
| // URL. |
| base::flat_set<GURL> triggered_; |
| }; |
| |
| PrerenderHostRegistryObserver::PrerenderHostRegistryObserver( |
| WebContents& web_contents) |
| : impl_(std::make_unique<PrerenderHostRegistryObserverImpl>(web_contents)) { |
| } |
| |
| PrerenderHostRegistryObserver::~PrerenderHostRegistryObserver() = default; |
| |
| void PrerenderHostRegistryObserver::WaitForTrigger(const GURL& url) { |
| TRACE_EVENT("test", "PrerenderHostRegistryObserver::WaitForTrigger", "url", |
| url); |
| impl_->WaitForTrigger(url); |
| } |
| |
| GURL PrerenderHostRegistryObserver::WaitForNextTrigger() { |
| TRACE_EVENT("test", "PrerenderHostRegistryObserver::WaitForNextTrigger"); |
| return impl_->WaitForNextTrigger(); |
| } |
| |
| void PrerenderHostRegistryObserver::NotifyOnTrigger( |
| const GURL& url, |
| base::OnceClosure callback) { |
| TRACE_EVENT("test", "PrerenderHostRegistryObserver::NotifyOnTrigger", "url", |
| url); |
| impl_->NotifyOnTrigger(url, std::move(callback)); |
| } |
| |
| base::flat_set<GURL> PrerenderHostRegistryObserver::GetTriggeredUrls() const { |
| return impl_->GetTriggeredUrls(); |
| } |
| |
| class PrerenderHostObserverImpl : public PrerenderHost::Observer { |
| public: |
| PrerenderHostObserverImpl(WebContents& web_contents, |
| FrameTreeNodeId host_id) { |
| PrerenderHost* host = GetPrerenderHostById(&web_contents, host_id); |
| DCHECK(host) |
| << "A PrerenderHost with the given id does not, or no longer, exists."; |
| StartObserving(*host); |
| } |
| |
| PrerenderHostObserverImpl(WebContents& web_contents, const GURL& url) { |
| registry_observer_ = |
| std::make_unique<PrerenderHostRegistryObserver>(web_contents); |
| if (PrerenderHost* host = GetPrerenderHostRegistry(&web_contents) |
| .FindHostByUrlForTesting(url)) { |
| StartObserving(*host); |
| } else { |
| registry_observer_->NotifyOnTrigger( |
| url, |
| base::BindOnce(&PrerenderHostObserverImpl::OnTrigger, |
| base::Unretained(this), std::ref(web_contents), url)); |
| } |
| } |
| |
| void OnActivated() override { |
| was_activated_ = true; |
| if (waiting_for_activation_) |
| std::move(waiting_for_activation_).Run(); |
| } |
| |
| void OnHeadersReceived(NavigationHandle& navigation_handle) override { |
| received_headers_ = true; |
| if (waiting_for_headers_) { |
| std::move(waiting_for_headers_).Run(); |
| } |
| } |
| |
| void OnHostDestroyed(PrerenderFinalStatus final_status) override { |
| observation_.Reset(); |
| last_status_ = final_status; |
| if (waiting_for_destruction_) { |
| std::move(waiting_for_destruction_).Run(); |
| } |
| EXPECT_FALSE(waiting_for_activation_) |
| << "A prerender was destroyed, with status " |
| << base::to_underlying(final_status) |
| << ", while waiting for activation."; |
| } |
| |
| void WaitForActivation() { |
| if (was_activated_) |
| return; |
| EXPECT_FALSE(waiting_for_activation_); |
| |
| EXPECT_FALSE(did_observe_ && !observation_.IsObserving()) |
| << "A prerender was destroyed, with status " |
| << base::to_underlying( |
| last_status_.value_or(PrerenderFinalStatus::kDestroyed)) |
| << ", before waiting for activation."; |
| |
| base::RunLoop loop; |
| waiting_for_activation_ = loop.QuitClosure(); |
| loop.Run(); |
| |
| EXPECT_TRUE(did_observe_) << "No prerender was triggered."; |
| } |
| |
| void WaitForHeaders() { |
| if (received_headers_) { |
| return; |
| } |
| EXPECT_FALSE(waiting_for_headers_); |
| |
| EXPECT_FALSE(did_observe_ && !observation_.IsObserving()) |
| << "A prerender was destroyed, with status " |
| << base::to_underlying( |
| last_status_.value_or(PrerenderFinalStatus::kDestroyed)) |
| << ", before waiting for headers."; |
| |
| base::RunLoop loop; |
| waiting_for_headers_ = loop.QuitClosure(); |
| loop.Run(); |
| |
| EXPECT_TRUE(did_observe_) << "No prerender was triggered."; |
| } |
| |
| void WaitForDestroyed() { |
| if (did_observe_ && !observation_.IsObserving()) |
| return; |
| EXPECT_FALSE(waiting_for_destruction_); |
| base::RunLoop loop; |
| waiting_for_destruction_ = loop.QuitClosure(); |
| loop.Run(); |
| } |
| |
| bool was_activated() const { return was_activated_; } |
| |
| bool WasHostReused() const { |
| return last_status_ == PrerenderFinalStatus::kPrerenderHostReused; |
| } |
| |
| private: |
| void OnTrigger(WebContents& web_contents, const GURL& url) { |
| PrerenderHost* host = |
| GetPrerenderHostRegistry(&web_contents).FindHostByUrlForTesting(url); |
| DCHECK(host) << "Attempted to trigger a prerender for [" << url << "] " |
| << "but canceled before a PrerenderHost was created."; |
| StartObserving(*host); |
| } |
| void StartObserving(PrerenderHost& host) { |
| did_observe_ = true; |
| observation_.Observe(&host); |
| |
| // This method may be bound and called from |registry_observer_| so don't |
| // add code below the reset. |
| registry_observer_.reset(); |
| } |
| |
| base::ScopedObservation<PrerenderHost, PrerenderHost::Observer> observation_{ |
| this}; |
| base::OnceClosure waiting_for_activation_; |
| base::OnceClosure waiting_for_headers_; |
| base::OnceClosure waiting_for_destruction_; |
| std::unique_ptr<PrerenderHostRegistryObserver> registry_observer_; |
| bool was_activated_ = false; |
| bool received_headers_ = false; |
| bool did_observe_ = false; |
| std::optional<PrerenderFinalStatus> last_status_; |
| }; |
| |
| PrerenderHostObserver::PrerenderHostObserver(WebContents& web_contents, |
| FrameTreeNodeId prerender_host) |
| : impl_(std::make_unique<PrerenderHostObserverImpl>(web_contents, |
| prerender_host)) {} |
| |
| PrerenderHostObserver::PrerenderHostObserver(WebContents& web_contents, |
| const GURL& url) |
| : impl_(std::make_unique<PrerenderHostObserverImpl>(web_contents, url)) {} |
| |
| PrerenderHostObserver::~PrerenderHostObserver() = default; |
| |
| void PrerenderHostObserver::WaitForActivation() { |
| TRACE_EVENT("test", "PrerenderHostObserver::WaitForActivation"); |
| impl_->WaitForActivation(); |
| } |
| |
| void PrerenderHostObserver::WaitForHeaders() { |
| TRACE_EVENT("test", "PrerenderHostObserver::WaitForHeaders"); |
| impl_->WaitForHeaders(); |
| } |
| |
| void PrerenderHostObserver::WaitForDestroyed() { |
| TRACE_EVENT("test", "PrerenderHostObserver::WaitForDestroyed"); |
| impl_->WaitForDestroyed(); |
| } |
| |
| bool PrerenderHostObserver::was_activated() const { |
| return impl_->was_activated(); |
| } |
| |
| bool PrerenderHostObserver::WasHostReused() const { |
| return impl_->WasHostReused(); |
| } |
| |
| PrerenderHostCreationWaiter::PrerenderHostCreationWaiter() { |
| PrerenderHost::SetHostCreationCallbackForTesting( |
| base::BindLambdaForTesting([&](FrameTreeNodeId host_id) { |
| created_host_id_ = host_id; |
| run_loop_.QuitClosure().Run(); |
| })); |
| } |
| |
| FrameTreeNodeId PrerenderHostCreationWaiter::Wait() { |
| EXPECT_TRUE(created_host_id_.is_null()); |
| run_loop_.Run(); |
| EXPECT_TRUE(created_host_id_); |
| return created_host_id_; |
| } |
| |
| ScopedPrerenderFeatureList::ScopedPrerenderFeatureList() |
| : ScopedPrerenderFeatureList(/*force_disable_prerender2_fallback=*/true) {} |
| |
| ScopedPrerenderFeatureList::ScopedPrerenderFeatureList( |
| bool force_disable_prerender2_fallback) { |
| std::vector<base::test::FeatureRef> enabled_features; |
| std::vector<base::test::FeatureRef> disabled_features; |
| |
| // Disable the memory requirement of Prerender2 |
| // so the test can run on any bot. |
| disabled_features.push_back(blink::features::kPrerender2MemoryControls); |
| |
| // In addition, disable `kPrerender2FallbackPrefetchSpecRules` if the user |
| // of `PrerenderTestHelper` is not ready for it as it changes |
| // `PrerenderFinalStatus` to `PrerenderFailedDuringPrefetch`, so that we |
| // enable it in fieldtrial testing config and then fix them one by one. |
| if (force_disable_prerender2_fallback) { |
| disabled_features.push_back(features::kPrerender2FallbackPrefetchSpecRules); |
| } |
| |
| feature_list_.InitWithFeatures(enabled_features, disabled_features); |
| } |
| |
| PrerenderTestHelper::PrerenderTestHelper(const WebContents::Getter& fn) |
| : feature_list_(ScopedPrerenderFeatureList( |
| /*force_disable_prerender2_fallback=*/true)), |
| get_web_contents_fn_(fn) {} |
| |
| PrerenderTestHelper::PrerenderTestHelper(const WebContents::Getter& fn, |
| bool force_disable_prerender2_fallback) |
| : feature_list_( |
| ScopedPrerenderFeatureList(force_disable_prerender2_fallback)), |
| get_web_contents_fn_(fn) {} |
| |
| PrerenderTestHelper::~PrerenderTestHelper() = default; |
| |
| void PrerenderTestHelper::RegisterServerRequestMonitor( |
| net::test_server::EmbeddedTestServer* http_server) { |
| EXPECT_FALSE(http_server->Started()); |
| http_server->RegisterRequestMonitor(base::BindRepeating( |
| &PrerenderTestHelper::MonitorResourceRequest, base::Unretained(this))); |
| } |
| void PrerenderTestHelper::RegisterServerRequestMonitor( |
| net::test_server::EmbeddedTestServer& test_server) { |
| EXPECT_FALSE(test_server.Started()); |
| test_server.RegisterRequestMonitor(base::BindRepeating( |
| &PrerenderTestHelper::MonitorResourceRequest, base::Unretained(this))); |
| } |
| |
| // static |
| FrameTreeNodeId PrerenderTestHelper::GetHostForUrl(WebContents& web_contents, |
| const GURL& url) { |
| auto* host = |
| GetPrerenderHostRegistry(&web_contents).FindHostByUrlForTesting(url); |
| return host ? host->frame_tree_node_id() : FrameTreeNodeId(); |
| } |
| |
| FrameTreeNodeId PrerenderTestHelper::GetHostForUrl(const GURL& url) { |
| return GetHostForUrl(*GetWebContents(), url); |
| } |
| |
| // static |
| FrameTreeNodeId PrerenderTestHelper::GetPrewarmSearchResultHost( |
| WebContents& web_contents, |
| const GURL& prewarm_url) { |
| auto* host = GetPrerenderHostRegistry(&web_contents) |
| .FindPrewarmSearchResultHostForTesting(prewarm_url); |
| return host ? host->frame_tree_node_id() : FrameTreeNodeId(); |
| } |
| |
| FrameTreeNodeId PrerenderTestHelper::GetPrewarmSearchResultHost( |
| const GURL& url) { |
| return GetPrewarmSearchResultHost(*GetWebContents(), url); |
| } |
| |
| bool PrerenderTestHelper::HasNewTabHandle(FrameTreeNodeId host_id) { |
| PrerenderHostRegistry& registry = GetPrerenderHostRegistry(GetWebContents()); |
| return registry.HasNewTabHandleByIdForTesting(host_id); |
| } |
| |
| void PrerenderTestHelper::WaitForPrerenderLoadCompletion( |
| FrameTreeNodeId host_id) { |
| TRACE_EVENT("test", "PrerenderTestHelper::WaitForPrerenderLoadCompletion", |
| "host_id", host_id); |
| auto* host = GetPrerenderHostById(GetWebContents(), host_id); |
| ASSERT_NE(host, nullptr); |
| auto status = host->WaitForLoadStopForTesting(); |
| EXPECT_EQ(status, PrerenderHost::LoadingOutcome::kLoadingCompleted); |
| } |
| |
| // static |
| void PrerenderTestHelper::WaitForPrerenderLoadCompletion( |
| WebContents& web_contents, |
| const GURL& url) { |
| auto status = WaitForPrerenderLoadingOutcome(web_contents, url); |
| EXPECT_EQ(status, PrerenderHost::LoadingOutcome::kLoadingCompleted); |
| } |
| |
| void PrerenderTestHelper::WaitForPrerenderLoadCompletion(const GURL& url) { |
| WaitForPrerenderLoadCompletion(*GetWebContents(), url); |
| } |
| |
| // static |
| void PrerenderTestHelper::WaitForPrerenderLoadCancellation( |
| WebContents& web_contents, |
| const GURL& url) { |
| auto status = WaitForPrerenderLoadingOutcome(web_contents, url); |
| EXPECT_EQ(status, PrerenderHost::LoadingOutcome::kPrerenderingCancelled); |
| } |
| |
| void PrerenderTestHelper::WaitForPrerenderLoadCancellation(const GURL& url) { |
| WaitForPrerenderLoadCancellation(*GetWebContents(), url); |
| } |
| |
| FrameTreeNodeId PrerenderTestHelper::AddPrerender(const GURL& prerendering_url, |
| int32_t world_id) { |
| return AddPrerender(prerendering_url, /*eagerness=*/std::nullopt, |
| /*target_hint=*/"", world_id); |
| } |
| |
| FrameTreeNodeId PrerenderTestHelper::AddPrerender( |
| const GURL& prerendering_url, |
| std::optional<blink::mojom::SpeculationEagerness> eagerness, |
| const std::string& target_hint, |
| int32_t world_id) { |
| return AddPrerender(prerendering_url, eagerness, |
| /*no_vary_search_hint=*/std::nullopt, target_hint, |
| /*ruleset_tag=*/std::nullopt, world_id); |
| } |
| |
| FrameTreeNodeId PrerenderTestHelper::AddPrerender( |
| const GURL& prerendering_url, |
| std::optional<blink::mojom::SpeculationEagerness> eagerness, |
| std::optional<std::string> no_vary_search_hint, |
| const std::string& target_hint, |
| std::optional<std::string> ruleset_tag, |
| int32_t world_id) { |
| TRACE_EVENT("test", "PrerenderTestHelper::AddPrerender", "prerendering_url", |
| prerendering_url); |
| EXPECT_TRUE(content::BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| |
| WebContents* prerender_web_contents = nullptr; |
| if (target_hint == "_blank") { |
| // Wait until AddPrerendersAsync() creates a new WebContents for |
| // prerendering. |
| base::RunLoop run_loop; |
| auto creation_subscription = content::RegisterWebContentsCreationCallback( |
| base::BindLambdaForTesting([&](content::WebContents* web_contents) { |
| prerender_web_contents = web_contents; |
| run_loop.QuitClosure().Run(); |
| })); |
| AddPrerendersAsync({prerendering_url}, eagerness, no_vary_search_hint, |
| target_hint, ruleset_tag, world_id); |
| run_loop.Run(); |
| } else { |
| // For other target hints, the initiator's WebContents will host a |
| // prerendered page. |
| prerender_web_contents = GetWebContents(); |
| AddPrerendersAsync({prerendering_url}, eagerness, no_vary_search_hint, |
| target_hint, ruleset_tag, world_id); |
| } |
| |
| WaitForPrerenderLoadCompletion(*prerender_web_contents, prerendering_url); |
| FrameTreeNodeId host_id = |
| GetHostForUrl(*prerender_web_contents, prerendering_url); |
| EXPECT_TRUE(host_id); |
| return host_id; |
| } |
| |
| void PrerenderTestHelper::AddPrerenderAsync(const GURL& prerendering_url, |
| int32_t world_id) { |
| AddPrerendersAsync({prerendering_url}, /*eagerness=*/std::nullopt, |
| /*target_hint=*/std::string(), world_id); |
| } |
| |
| void PrerenderTestHelper::AddPrerendersAsync( |
| const std::vector<GURL>& prerendering_urls, |
| std::optional<blink::mojom::SpeculationEagerness> eagerness, |
| const std::string& target_hint, |
| int32_t world_id) { |
| AddPrerendersAsync(prerendering_urls, eagerness, |
| /*no_vary_search_hint=*/std::nullopt, target_hint, |
| /*ruleset_tag=*/std::nullopt, world_id); |
| } |
| |
| void PrerenderTestHelper::AddPrerendersAsync( |
| const std::vector<GURL>& prerendering_urls, |
| std::optional<blink::mojom::SpeculationEagerness> eagerness, |
| std::optional<std::string> no_vary_search_hint, |
| const std::string& target_hint, |
| std::optional<std::string> ruleset_tag, |
| int32_t world_id) { |
| TRACE_EVENT( |
| "test", "PrerenderTestHelper::AddPrerendersAsync", "prerendering_urls", |
| prerendering_urls, "eagerness", |
| eagerness.has_value() ? ConvertEagernessToString(eagerness.value()) |
| : "(empty)", |
| "expected_no_vary_search", |
| no_vary_search_hint.has_value() ? no_vary_search_hint.value() : "(empty)", |
| "target_hint", target_hint.empty() ? "(empty)" : target_hint); |
| EXPECT_TRUE(content::BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| std::string script = BuildScriptElementSpeculationRules( |
| prerendering_urls, eagerness, no_vary_search_hint, target_hint, |
| ruleset_tag); |
| |
| if (world_id == ISOLATED_WORLD_ID_GLOBAL) { |
| // Have to use ExecuteJavaScriptForTests instead of ExecJs/EvalJs here, |
| // because some test pages have ContentSecurityPolicy and EvalJs cannot work |
| // with it. See the quick migration guide for EvalJs for more information. |
| GetWebContents()->GetPrimaryMainFrame()->ExecuteJavaScriptForTests( |
| base::UTF8ToUTF16(script), base::NullCallback(), world_id); |
| } else { |
| GetWebContents()->GetPrimaryMainFrame()->ExecuteJavaScriptInIsolatedWorld( |
| base::UTF8ToUTF16(script), base::NullCallback(), world_id); |
| } |
| } |
| |
| void PrerenderTestHelper::AddPrerenderUntilScriptAsync( |
| const GURL& url, |
| blink::mojom::SpeculationEagerness eagerness) { |
| EXPECT_TRUE(content::BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| std::string script = JsReplace(kAddSpeculationRulePrerenderUntilScriptScript, |
| url, ConvertEagernessToString(eagerness)); |
| |
| // Have to use ExecuteJavaScriptForTests instead of ExecJs/EvalJs here, |
| // because some test pages have ContentSecurityPolicy and EvalJs cannot work |
| // with it. See the quick migration guide for EvalJs for more information. |
| GetWebContents()->GetPrimaryMainFrame()->ExecuteJavaScriptForTests( |
| base::UTF8ToUTF16(script), base::NullCallback(), |
| ISOLATED_WORLD_ID_GLOBAL); |
| } |
| |
| void PrerenderTestHelper::AddPrefetchAsync(const GURL& prefetch_url) { |
| EXPECT_TRUE(content::BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| std::string script = |
| JsReplace(kAddSpeculationRulePrefetchScript, prefetch_url); |
| |
| // Have to use ExecuteJavaScriptForTests instead of ExecJs/EvalJs here, |
| // because some test pages have ContentSecurityPolicy and EvalJs cannot work |
| // with it. See the quick migration guide for EvalJs for more information. |
| GetWebContents()->GetPrimaryMainFrame()->ExecuteJavaScriptForTests( |
| base::UTF8ToUTF16(script), base::NullCallback(), |
| ISOLATED_WORLD_ID_GLOBAL); |
| } |
| |
| std::unique_ptr<PrerenderHandle> |
| PrerenderTestHelper::AddEmbedderTriggeredPrerenderAsync( |
| WebContents& web_contents, |
| const GURL& prerendering_url, |
| PreloadingTriggerType trigger_type, |
| const std::string& embedder_histogram_suffix, |
| ui::PageTransition page_transition) { |
| TRACE_EVENT("test", "PrerenderTestHelper::AddEmbedderTriggeredPrerenderAsync", |
| "prerendering_url", prerendering_url, "trigger_type", |
| trigger_type, "embedder_histogram_suffix", |
| embedder_histogram_suffix, "page_transition", page_transition); |
| if (!content::BrowserThread::CurrentlyOn(BrowserThread::UI)) |
| return nullptr; |
| |
| return web_contents.StartPrerendering( |
| prerendering_url, trigger_type, embedder_histogram_suffix, |
| /*additional_headers=*/net::HttpRequestHeaders(), |
| /*no_vary_search_hint=*/std::nullopt, page_transition, |
| /*should_warm_up_compositor=*/false, |
| /*should_prepare_paint_tree=*/false, |
| PreloadingHoldbackStatus::kUnspecified, |
| PreloadPipelineInfo::Create( |
| /*planned_max_preloading_type=*/PreloadingType::kPrerender), |
| /*preloading_attempt=*/nullptr, /*url_match_predicate=*/{}, |
| /*prerender_navigation_handle_callback=*/{}, |
| /*allow_reuse=*/false); |
| } |
| |
| std::unique_ptr<PrerenderHandle> |
| PrerenderTestHelper::AddEmbedderTriggeredPrerenderAsync( |
| const GURL& prerendering_url, |
| PreloadingTriggerType trigger_type, |
| const std::string& embedder_histogram_suffix, |
| ui::PageTransition page_transition) { |
| return AddEmbedderTriggeredPrerenderAsync( |
| *GetWebContents(), prerendering_url, trigger_type, |
| embedder_histogram_suffix, page_transition); |
| } |
| |
| void PrerenderTestHelper::NavigatePrerenderedPage(FrameTreeNodeId host_id, |
| const GURL& url) { |
| TRACE_EVENT("test", "PrerenderTestHelper::NavigatePrerenderedPage", "host_id", |
| host_id, "url", url); |
| |
| // Take RenderFrameHost corresponding to the main frame of the prerendered |
| // page. |
| auto* prerender_web_contents = WebContents::FromFrameTreeNodeId(host_id); |
| auto* prerender_host = GetPrerenderHostById(prerender_web_contents, host_id); |
| ASSERT_NE(prerender_host, nullptr); |
| RenderFrameHostImpl* prerender_render_frame_host = |
| prerender_host->GetPrerenderedMainFrameHost(); |
| |
| // Navigate the RenderFrameHost to the URL. |
| // |
| // Ignore the result of ExecJs() to avoid unexpected execution failure. |
| // Navigation from the prerendered page could cancel prerendering and destroy |
| // the prerendered frame before ExecJs() gets a result from that. This results |
| // in execution failure even when the execution succeeded. |
| // See https://crbug.com/1186584 for details. |
| std::ignore = |
| ExecJs(prerender_render_frame_host, JsReplace("location = $1", url)); |
| } |
| |
| void PrerenderTestHelper::CancelPrerenderedPage(FrameTreeNodeId host_id) { |
| PrerenderHostRegistry& registry = GetPrerenderHostRegistry(GetWebContents()); |
| registry.CancelHost(host_id, PrerenderFinalStatus::kDestroyed); |
| } |
| |
| // static |
| std::unique_ptr<content::TestNavigationObserver> |
| PrerenderTestHelper::NavigatePrimaryPageAsync(WebContents& web_contents, |
| const GURL& url, |
| ui::PageTransition transition) { |
| TRACE_EVENT("test", "PrerenderTestHelper::NavigatePrimaryPage", |
| "web_contents", web_contents, "url", url); |
| const bool is_renderer_initiated = |
| PageTransitionCoreTypeIs(transition, ui::PAGE_TRANSITION_LINK); |
| if (is_renderer_initiated && web_contents.IsLoading()) { |
| // Ensure that any ongoing navigation is complete prior to the construction |
| // of |observer| below (this navigation may complete while executing ExecJs |
| // machinery). |
| // Skip this wait when testing browser initiated navigations which don't |
| // expect this wait. |
| content::TestNavigationObserver initial_observer(&web_contents); |
| initial_observer.set_wait_event( |
| content::TestNavigationObserver::WaitEvent::kLoadStopped); |
| initial_observer.Wait(); |
| } |
| |
| EXPECT_TRUE(content::BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| std::unique_ptr<content::TestNavigationObserver> observer = |
| std::make_unique<content::TestNavigationObserver>(&web_contents); |
| observer->set_wait_event( |
| content::TestNavigationObserver::WaitEvent::kLoadStopped); |
| if (is_renderer_initiated) { |
| // Ignore the result of ExecJs(). |
| // |
| // Depending on timing, activation could destroy a navigating frame before |
| // ExecJs() gets a result from the frame. This results in execution failure |
| // even when the navigation succeeded. |
| std::ignore = ExecJs(web_contents.GetPrimaryMainFrame(), |
| JsReplace("location = $1", url)); |
| } else { |
| web_contents.OpenURL( |
| OpenURLParams(url, Referrer(), WindowOpenDisposition::CURRENT_TAB, |
| transition, is_renderer_initiated), |
| /*navigation_handle_callback=*/{}); |
| } |
| return observer; |
| } |
| |
| std::unique_ptr<content::TestNavigationObserver> |
| PrerenderTestHelper::NavigatePrimaryPageAsync(const GURL& url, |
| ui::PageTransition transition) { |
| return NavigatePrimaryPageAsync(*GetWebContents(), url, transition); |
| } |
| |
| // static |
| void PrerenderTestHelper::NavigatePrimaryPage(WebContents& web_contents, |
| const GURL& url, |
| ui::PageTransition transition) { |
| NavigatePrimaryPageAsync(web_contents, url, transition)->Wait(); |
| } |
| |
| void PrerenderTestHelper::NavigatePrimaryPage(const GURL& url, |
| ui::PageTransition transition) { |
| NavigatePrimaryPage(*GetWebContents(), url, transition); |
| } |
| |
| void PrerenderTestHelper::OpenNewWindowWithoutOpener(WebContents& web_contents, |
| const GURL& url) { |
| std::string script = R"(window.open($1, "_blank", "noopener");)"; |
| EXPECT_TRUE(ExecJs(&web_contents, JsReplace(script, url.spec()))); |
| } |
| |
| void PrerenderTestHelper::SetHoldback(PreloadingType preloading_type, |
| PreloadingPredictor predictor, |
| bool holdback) { |
| preloading_config_override_.SetHoldback(preloading_type, predictor, holdback); |
| } |
| |
| void PrerenderTestHelper::SetHoldback(std::string_view preloading_type, |
| std::string_view predictor, |
| bool holdback) { |
| preloading_config_override_.SetHoldback(preloading_type, predictor, holdback); |
| } |
| |
| ::testing::AssertionResult PrerenderTestHelper::VerifyPrerenderingState( |
| const GURL& url) { |
| PrerenderHostRegistry& registry = GetPrerenderHostRegistry(GetWebContents()); |
| PrerenderHost* prerender_host = registry.FindHostByUrlForTesting(url); |
| RenderFrameHostImpl* prerendered_render_frame_host = |
| prerender_host->GetPrerenderedMainFrameHost(); |
| std::vector<RenderFrameHost*> frames = |
| CollectAllRenderFrameHosts(prerendered_render_frame_host); |
| for (auto* frame : frames) { |
| auto* rfhi = static_cast<RenderFrameHostImpl*>(frame); |
| // All the subframes should be in LifecycleStateImpl::kPrerendering state |
| // before activation. |
| if (rfhi->lifecycle_state() != |
| RenderFrameHostImpl::LifecycleStateImpl::kPrerendering) { |
| return ::testing::AssertionFailure() << "subframe in incorrect state"; |
| } |
| } |
| |
| // Make sure that all the PrerenderHost frame trees are prerendering. |
| const std::vector<FrameTree*> prerender_frame_trees = |
| registry.GetPrerenderFrameTrees(); |
| std::for_each(std::begin(prerender_frame_trees), |
| std::end(prerender_frame_trees), [](auto const& frame_tree) { |
| ASSERT_TRUE(frame_tree->is_prerendering()); |
| }); |
| |
| return ::testing::AssertionSuccess(); |
| } |
| |
| // static |
| RenderFrameHost* PrerenderTestHelper::GetPrerenderedMainFrameHost( |
| WebContents& web_contents, |
| FrameTreeNodeId host_id) { |
| auto* prerender_host = GetPrerenderHostById(&web_contents, host_id); |
| EXPECT_NE(prerender_host, nullptr); |
| return prerender_host->GetPrerenderedMainFrameHost(); |
| } |
| |
| // static |
| RenderFrameHost* PrerenderTestHelper::GetPrerenderedMainFrameHost( |
| WebContents& web_contents, |
| const GURL& url) { |
| auto* prerender_host = GetPrerenderHostByUrl(&web_contents, url); |
| EXPECT_NE(prerender_host, nullptr); |
| return prerender_host->GetPrerenderedMainFrameHost(); |
| } |
| |
| RenderFrameHost* PrerenderTestHelper::GetPrerenderedMainFrameHost( |
| FrameTreeNodeId host_id) { |
| return GetPrerenderedMainFrameHost(*GetWebContents(), host_id); |
| } |
| |
| RenderFrameHost* PrerenderTestHelper::GetPrerenderedMainFrameHost( |
| const GURL& url) { |
| return GetPrerenderedMainFrameHost(*GetWebContents(), url); |
| } |
| |
| int PrerenderTestHelper::GetRequestCount(const GURL& url) { |
| EXPECT_TRUE(content::BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| base::AutoLock auto_lock(lock_); |
| return request_count_by_path_[url.PathForRequest()]; |
| } |
| |
| net::test_server::HttpRequest::HeaderMap PrerenderTestHelper::GetRequestHeaders( |
| const GURL& url) { |
| EXPECT_TRUE(content::BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| base::AutoLock auto_lock(lock_); |
| std::string path = url.PathForRequest(); |
| DCHECK(base::Contains(request_headers_by_path_, path)) << path; |
| return request_headers_by_path_[path]; |
| } |
| |
| void PrerenderTestHelper::WaitForRequest(const GURL& url, int count) { |
| TRACE_EVENT("test", "PrerenderTestHelper::WaitForRequest", "url", url, |
| "count", count); |
| for (;;) { |
| base::RunLoop run_loop; |
| { |
| base::AutoLock auto_lock(lock_); |
| if (request_count_by_path_[url.PathForRequest()] >= count) |
| return; |
| monitor_callback_ = run_loop.QuitClosure(); |
| } |
| run_loop.Run(); |
| } |
| } |
| |
| void PrerenderTestHelper::MonitorResourceRequest( |
| const net::test_server::HttpRequest& request) { |
| // This should be called on `EmbeddedTestServer::io_thread_`. |
| EXPECT_FALSE(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); |
| base::AutoLock auto_lock(lock_); |
| request_count_by_path_[request.GetURL().PathForRequest()]++; |
| request_headers_by_path_.emplace(request.GetURL().PathForRequest(), |
| request.headers); |
| if (monitor_callback_) |
| std::move(monitor_callback_).Run(); |
| } |
| |
| WebContents* PrerenderTestHelper::GetWebContents() { |
| return get_web_contents_fn_.Run(); |
| } |
| |
| std::string PrerenderTestHelper::GenerateHistogramName( |
| const std::string& histogram_base_name, |
| content::PreloadingTriggerType trigger_type, |
| const std::string& embedder_suffix) { |
| switch (trigger_type) { |
| case content::PreloadingTriggerType::kSpeculationRule: |
| DCHECK(embedder_suffix.empty()); |
| return std::string(histogram_base_name) + ".SpeculationRule"; |
| case content::PreloadingTriggerType::kSpeculationRuleFromIsolatedWorld: |
| DCHECK(embedder_suffix.empty()); |
| return std::string(histogram_base_name) + |
| ".SpeculationRuleFromIsolatedWorld"; |
| case content::PreloadingTriggerType:: |
| kSpeculationRuleFromAutoSpeculationRules: |
| DCHECK(embedder_suffix.empty()); |
| return std::string(histogram_base_name) + |
| ".SpeculationRuleFromAutoSpeculationRules"; |
| case content::PreloadingTriggerType::kEmbedder: |
| DCHECK(!embedder_suffix.empty()); |
| return std::string(histogram_base_name) + ".Embedder_" + embedder_suffix; |
| } |
| NOTREACHED(); |
| } |
| |
| ScopedPrerenderWebContentsDelegate::ScopedPrerenderWebContentsDelegate( |
| WebContents& web_contents) |
| : web_contents_(web_contents.GetWeakPtr()) { |
| web_contents_->SetDelegate(this); |
| } |
| |
| ScopedPrerenderWebContentsDelegate::~ScopedPrerenderWebContentsDelegate() { |
| if (web_contents_) |
| web_contents_.get()->SetDelegate(nullptr); |
| } |
| |
| PreloadingEligibility ScopedPrerenderWebContentsDelegate::IsPrerender2Supported( |
| WebContents& web_contents, |
| PreloadingTriggerType trigger_type) { |
| return PreloadingEligibility::kEligible; |
| } |
| |
| MockLinkPreviewWebContentsDelegate::MockLinkPreviewWebContentsDelegate() = |
| default; |
| |
| MockLinkPreviewWebContentsDelegate::~MockLinkPreviewWebContentsDelegate() = |
| default; |
| |
| } // namespace test |
| |
| } // namespace content |