blob: 834b58cd07610ccf6f7d0bcfe7b4ce982e048d3e [file] [log] [blame]
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/optimization_guide/content/browser/page_text_observer.h"
#include <string>
#include "base/bind.h"
#include "base/callback.h"
#include "base/optional.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/optimization_guide/content/mojom/page_text_service.mojom.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_base.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test_utils.h"
#include "net/dns/mock_host_resolver.h"
#include "net/http/http_status_code.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "testing/gmock/include/gmock/gmock.h"
namespace optimization_guide {
namespace {
FrameTextDumpResult MakeFrameDump(mojom::TextDumpEvent event,
content::GlobalFrameRoutingId rfh_id,
bool amp_frame,
int nav_id,
const std::u16string& contents) {
return FrameTextDumpResult::Initialize(event, rfh_id, amp_frame, nav_id)
.CompleteWithContents(contents);
}
class TestConsumer : public PageTextObserver::Consumer {
public:
TestConsumer() = default;
~TestConsumer() = default;
void Reset() { was_called_ = false; }
void PopulateRequest(uint32_t max_size,
const std::set<mojom::TextDumpEvent>& events,
bool request_amp = false) {
request_ = std::make_unique<PageTextObserver::ConsumerTextDumpRequest>();
request_->max_size = max_size;
request_->events = events;
request_->callback =
base::BindOnce(&TestConsumer::OnGotTextDump, base::Unretained(this));
request_->dump_amp_subframes = request_amp;
}
void WaitForPageText() {
if (result_) {
return;
}
base::RunLoop run_loop;
on_page_text_closure_ = run_loop.QuitClosure();
run_loop.Run();
}
bool was_called() const { return was_called_; }
base::Optional<PageTextDumpResult> result() {
return base::OptionalFromPtr(result_.get());
}
// PageTextObserver::Consumer:
std::unique_ptr<PageTextObserver::ConsumerTextDumpRequest>
MaybeRequestFrameTextDump(content::NavigationHandle* handle) override {
was_called_ = true;
return std::move(request_);
}
private:
void OnGotTextDump(const PageTextDumpResult& result) {
result_ = std::make_unique<PageTextDumpResult>(result);
if (on_page_text_closure_) {
std::move(on_page_text_closure_).Run();
}
}
bool was_called_ = false;
std::unique_ptr<PageTextObserver::ConsumerTextDumpRequest> request_;
base::OnceClosure on_page_text_closure_;
std::unique_ptr<PageTextDumpResult> result_;
};
} // namespace
// This tests code in
// //components/optimization_guide/content/browser/page_text_observer.h, but
// this test is in //chrome because the components browsertests do not fully
// standup a renderer process.
class PageTextObserverBrowserTest : public InProcessBrowserTest {
public:
PageTextObserverBrowserTest() = default;
~PageTextObserverBrowserTest() override = default;
void SetUpOnMainThread() override {
host_resolver()->AddRule("*", "127.0.0.1");
InProcessBrowserTest::SetUpOnMainThread();
embedded_test_server()->RegisterRequestHandler(base::BindRepeating(
&PageTextObserverBrowserTest::RequestHandler, base::Unretained(this)));
embedded_test_server()->ServeFilesFromSourceDirectory(
"chrome/test/data/optimization_guide");
ASSERT_TRUE(embedded_test_server()->Start());
}
content::WebContents* web_contents() {
return browser()->tab_strip_model()->GetActiveWebContents();
}
PageTextObserver* observer() {
return PageTextObserver::FromWebContents(web_contents());
}
protected:
std::string dynamic_response_body_;
private:
std::unique_ptr<net::test_server::HttpResponse> RequestHandler(
const net::test_server::HttpRequest& request) {
std::string path_value;
// This script is render blocking in the HTML, but is intentionally slow.
// This provides important time between commit and first layout for any text
// dump requests to make it to the renderer, reducing flakes.
if (request.GetURL().path() == "/slow-first-layout.js") {
std::unique_ptr<net::test_server::DelayedHttpResponse> resp =
std::make_unique<net::test_server::DelayedHttpResponse>(
base::TimeDelta::FromMilliseconds(500));
resp->set_code(net::HTTP_OK);
resp->set_content_type("application/javascript");
resp->set_content(std::string());
return resp;
}
// This script is onLoad-blocking in the HTML, but is intentionally slow.
// This provides important time between first layout and finish load for
// tests that need it.
if (request.GetURL().path() == "/slow-add-world-text.js") {
std::unique_ptr<net::test_server::DelayedHttpResponse> resp =
std::make_unique<net::test_server::DelayedHttpResponse>(
base::TimeDelta::FromMilliseconds(500));
resp->set_code(net::HTTP_OK);
resp->set_content_type("application/javascript");
resp->set_content(
"var p = document.createElement('p'); p.innerHTML = 'world'; "
"document.body.appendChild(p); ");
return resp;
}
if (request.GetURL().path() == "/dynamic.html") {
std::unique_ptr<net::test_server::BasicHttpResponse> resp =
std::make_unique<net::test_server::BasicHttpResponse>();
resp->set_code(net::HTTP_OK);
resp->set_content_type("text/html");
resp->set_content(dynamic_response_body_);
return resp;
}
return nullptr;
}
};
IN_PROC_BROWSER_TEST_F(PageTextObserverBrowserTest, SimpleCaseNoSubframes) {
PageTextObserver::CreateForWebContents(web_contents());
ASSERT_TRUE(observer());
TestConsumer consumer;
observer()->AddConsumer(&consumer);
consumer.PopulateRequest(/*max_size=*/1024,
/*events=*/{mojom::TextDumpEvent::kFirstLayout});
GURL url(embedded_test_server()->GetURL("a.com", "/hello.html"));
ui_test_utils::NavigateToURL(browser(), url);
ASSERT_TRUE(consumer.was_called());
consumer.WaitForPageText();
ASSERT_TRUE(consumer.result());
EXPECT_THAT(
consumer.result()->frame_results(),
::testing::UnorderedElementsAreArray({
MakeFrameDump(
mojom::TextDumpEvent::kFirstLayout,
web_contents()->GetMainFrame()->GetGlobalFrameRoutingId(),
/*amp_frame=*/false,
web_contents()->GetController().GetVisibleEntry()->GetUniqueID(),
u"hello"),
}));
}
IN_PROC_BROWSER_TEST_F(PageTextObserverBrowserTest, FirstLayoutAndOnLoad) {
PageTextObserver::CreateForWebContents(web_contents());
ASSERT_TRUE(observer());
// This test can be flaky (crbug.com/1187264), and it always seems to be
// caused by the renderer never finishing the page load and is thus outside
// the control of this feature. Thorough testing shows that this flake does
// not repeat itself, so running the test an extra time is sufficient.
//
// If this test starts timing out or failing the |EXPECT_THAT| check, then
// that is indicative of a real issue.
for (size_t i = 0; i < 2; i++) {
TestConsumer first_layout_consumer;
TestConsumer on_load_consumer;
observer()->AddConsumer(&first_layout_consumer);
observer()->AddConsumer(&on_load_consumer);
first_layout_consumer.PopulateRequest(
/*max_size=*/1024,
/*events=*/{mojom::TextDumpEvent::kFirstLayout});
on_load_consumer.PopulateRequest(
/*max_size=*/1024,
/*events=*/{mojom::TextDumpEvent::kFinishedLoad});
GURL url(embedded_test_server()->GetURL("a.com", "/hello_world.html"));
ui_test_utils::NavigateToURL(browser(), url);
ASSERT_TRUE(first_layout_consumer.was_called());
ASSERT_TRUE(on_load_consumer.was_called());
first_layout_consumer.WaitForPageText();
on_load_consumer.WaitForPageText();
if (observer()->outstanding_requests() > 0) {
// This is a flake. Reset for the next attempt.
observer()->RemoveConsumer(&first_layout_consumer);
observer()->RemoveConsumer(&on_load_consumer);
// A new navigation should unblock any missing requests, which are then
// discarded.
ui_test_utils::NavigateToURL(browser(), GURL("about:blank"));
continue;
}
ASSERT_TRUE(first_layout_consumer.result());
EXPECT_THAT(
first_layout_consumer.result()->frame_results(),
::testing::UnorderedElementsAreArray({
MakeFrameDump(
mojom::TextDumpEvent::kFirstLayout,
web_contents()->GetMainFrame()->GetGlobalFrameRoutingId(),
/*amp_frame=*/false,
web_contents()
->GetController()
.GetVisibleEntry()
->GetUniqueID(),
u"hello"),
MakeFrameDump(
mojom::TextDumpEvent::kFinishedLoad,
web_contents()->GetMainFrame()->GetGlobalFrameRoutingId(),
/*amp_frame=*/false,
web_contents()
->GetController()
.GetVisibleEntry()
->GetUniqueID(),
u"hello\n\nworld"),
}));
EXPECT_EQ(first_layout_consumer.result(), on_load_consumer.result());
return;
}
FAIL();
}
IN_PROC_BROWSER_TEST_F(PageTextObserverBrowserTest, OOPIFAMPSubframe) {
if (content::IsIsolatedOriginRequiredToGuaranteeDedicatedProcess()) {
// Isolate b.com so that it is guaranteed to be in a different process.
content::IsolateOriginsForTesting(
embedded_test_server(),
browser()->tab_strip_model()->GetActiveWebContents(), {"b.com"});
}
PageTextObserver::CreateForWebContents(web_contents());
ASSERT_TRUE(observer());
TestConsumer consumer;
observer()->AddConsumer(&consumer);
consumer.PopulateRequest(
/*max_size=*/1024,
/*events=*/{mojom::TextDumpEvent::kFirstLayout},
/*request_amp=*/true);
GURL url(embedded_test_server()->GetURL("a.com", "/dynamic.html"));
dynamic_response_body_ = base::StringPrintf(
"<html><body>"
"<script type=\"text/javascript\" src=\"/slow-first-layout.js\"></script>"
"<p>mainframe</p>"
"<iframe name=\"amp\" src=\"%s\"></iframe>"
"</body></html>",
embedded_test_server()->GetURL("b.com", "/amp.html").spec().c_str());
ui_test_utils::NavigateToURL(browser(), url);
ASSERT_TRUE(consumer.was_called());
consumer.WaitForPageText();
content::GlobalFrameRoutingId amp_frame_id;
for (auto* rfh : web_contents()->GetMainFrame()->GetFramesInSubtree()) {
if (rfh->GetFrameName() == "amp") {
amp_frame_id = rfh->GetGlobalFrameRoutingId();
break;
}
}
ASSERT_TRUE(consumer.result());
EXPECT_THAT(
consumer.result()->frame_results(),
::testing::UnorderedElementsAreArray({
MakeFrameDump(
mojom::TextDumpEvent::kFirstLayout,
web_contents()->GetMainFrame()->GetGlobalFrameRoutingId(),
/*amp_frame=*/false,
web_contents()->GetController().GetVisibleEntry()->GetUniqueID(),
u"mainframe"),
MakeFrameDump(
mojom::TextDumpEvent::kFinishedLoad, amp_frame_id,
/*amp_frame=*/true,
web_contents()->GetController().GetVisibleEntry()->GetUniqueID(),
u"AMP"),
}));
}
IN_PROC_BROWSER_TEST_F(PageTextObserverBrowserTest, OOPIFNotAmpSubframe) {
PageTextObserver::CreateForWebContents(web_contents());
ASSERT_TRUE(observer());
TestConsumer consumer;
observer()->AddConsumer(&consumer);
consumer.PopulateRequest(
/*max_size=*/1024,
/*events=*/{mojom::TextDumpEvent::kFirstLayout},
/*request_amp=*/true);
GURL url(embedded_test_server()->GetURL("a.com", "/dynamic.html"));
dynamic_response_body_ = base::StringPrintf(
"<html><body>"
"<script type=\"text/javascript\" src=\"/slow-first-layout.js\"></script>"
"<p>mainframe</p>"
"<iframe src=\"%s\"></iframe>"
"</body></html>",
embedded_test_server()->GetURL("b.com", "/hello.html").spec().c_str());
ui_test_utils::NavigateToURL(browser(), url);
ASSERT_TRUE(consumer.was_called());
consumer.WaitForPageText();
ASSERT_TRUE(consumer.result());
EXPECT_THAT(
consumer.result()->frame_results(),
::testing::UnorderedElementsAreArray({
MakeFrameDump(
mojom::TextDumpEvent::kFirstLayout,
web_contents()->GetMainFrame()->GetGlobalFrameRoutingId(),
/*amp_frame=*/false,
web_contents()->GetController().GetVisibleEntry()->GetUniqueID(),
u"mainframe"),
}));
}
class PageTextObserverSingleProcessBrowserTest
: public PageTextObserverBrowserTest {
public:
PageTextObserverSingleProcessBrowserTest() = default;
~PageTextObserverSingleProcessBrowserTest() override = default;
void SetUpCommandLine(base::CommandLine* cmd_line) override {
PageTextObserverBrowserTest::SetUpCommandLine(cmd_line);
cmd_line->AppendSwitch("single-process");
}
};
#if defined(OS_MAC)
// https://crbug.com/1189556
#define MAYBE_SameProcessIframe DISABLED_SameProcessIframe
#else
#define MAYBE_SameProcessIframe SameProcessIframe
#endif
IN_PROC_BROWSER_TEST_F(PageTextObserverSingleProcessBrowserTest,
MAYBE_SameProcessIframe) {
PageTextObserver::CreateForWebContents(web_contents());
ASSERT_TRUE(observer());
TestConsumer consumer;
observer()->AddConsumer(&consumer);
consumer.PopulateRequest(/*max_size=*/1024,
/*events=*/{mojom::TextDumpEvent::kFinishedLoad});
GURL url(embedded_test_server()->GetURL("a.com", "/dynamic.html"));
dynamic_response_body_ = base::StringPrintf(
"<html><body>"
"<script type=\"text/javascript\" src=\"/slow-first-layout.js\"></script>"
"<p>mainframe</p>"
"<iframe src=\"%s\"></iframe>"
"</body></html>",
embedded_test_server()->GetURL("a.com", "/hello.html").spec().c_str());
ui_test_utils::NavigateToURL(browser(), url);
ASSERT_TRUE(consumer.was_called());
consumer.WaitForPageText();
ASSERT_TRUE(consumer.result());
EXPECT_THAT(
consumer.result()->frame_results(),
::testing::UnorderedElementsAreArray({
MakeFrameDump(
mojom::TextDumpEvent::kFinishedLoad,
web_contents()->GetMainFrame()->GetGlobalFrameRoutingId(),
/*amp_frame=*/false,
web_contents()->GetController().GetVisibleEntry()->GetUniqueID(),
u"mainframe\n\nhello"),
}));
}
IN_PROC_BROWSER_TEST_F(PageTextObserverSingleProcessBrowserTest,
SameProcessAMPSubframe) {
PageTextObserver::CreateForWebContents(web_contents());
ASSERT_TRUE(observer());
TestConsumer consumer;
observer()->AddConsumer(&consumer);
consumer.PopulateRequest(
/*max_size=*/1024,
/*events=*/{mojom::TextDumpEvent::kFirstLayout},
/*request_amp=*/true);
GURL url(embedded_test_server()->GetURL("a.com", "/dynamic.html"));
dynamic_response_body_ = base::StringPrintf(
"<html><body>"
"<script type=\"text/javascript\" src=\"/slow-first-layout.js\"></script>"
"<p>mainframe</p>"
"<iframe src=\"%s\"></iframe>"
"</body></html>",
embedded_test_server()->GetURL("a.com", "/amp.html").spec().c_str());
ui_test_utils::NavigateToURL(browser(), url);
ASSERT_TRUE(consumer.was_called());
consumer.WaitForPageText();
ASSERT_TRUE(consumer.result());
EXPECT_THAT(
consumer.result()->frame_results(),
::testing::UnorderedElementsAreArray({
MakeFrameDump(
mojom::TextDumpEvent::kFirstLayout,
web_contents()->GetMainFrame()->GetGlobalFrameRoutingId(),
/*amp_frame=*/false,
web_contents()->GetController().GetVisibleEntry()->GetUniqueID(),
u"mainframe"),
}));
}
} // namespace optimization_guide