blob: 8b2a173859ccc201850f7248097542e7c46ca1bd [file] [log] [blame]
// Copyright 2017 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 <memory>
#include <utility>
#include "base/bind.h"
#include "base/callback.h"
#include "base/command_line.h"
#include "base/memory/scoped_refptr.h"
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "build/build_config.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/thumbnails/thumbnail_service.h"
#include "chrome/browser/thumbnails/thumbnail_service_factory.h"
#include "chrome/browser/thumbnails/thumbnailing_context.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/keyed_service/content/browser_context_dependency_manager.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/common/content_switches.h"
#include "net/test/embedded_test_server/controllable_http_response.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkColor.h"
#include "url/gurl.h"
// These tests use --disable-gpu which isn't supported on ChromeOS.
#if !defined(OS_CHROMEOS)
namespace thumbnails {
namespace {
using testing::AtMost;
using testing::DoAll;
using testing::Field;
using testing::NiceMock;
using testing::Not;
using testing::Return;
using testing::_;
ACTION_P(RunClosure, closure) {
closure.Run();
}
// |arg| is a gfx::Image, |expected_color| is a SkColor.
MATCHER_P(ImageColorIs, expected_color, "") {
SkBitmap bitmap = arg.AsBitmap();
if (bitmap.empty()) {
*result_listener << "expected color but no bitmap data available";
return false;
}
SkColor actual_color = bitmap.getColor(1, 1);
if (actual_color != expected_color) {
*result_listener << "expected color "
<< base::StringPrintf("%08X", expected_color)
<< " but actual color is "
<< base::StringPrintf("%08X", actual_color);
return false;
}
return true;
}
const char kHttpResponseHeader[] =
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/html; charset=utf-8\r\n"
"\r\n";
std::string MakeHtmlDocument(const std::string& background_color) {
return base::StringPrintf(
"<html>"
"<head><style>body { background-color: %s }</style></head>"
"<body></body>"
"</html>",
background_color.c_str());
}
class MockThumbnailService : public ThumbnailService {
public:
MOCK_METHOD2(SetPageThumbnail,
bool(const ThumbnailingContext& context,
const gfx::Image& thumbnail));
MOCK_METHOD3(GetPageThumbnail,
bool(const GURL& url,
bool prefix_match,
scoped_refptr<base::RefCountedMemory>* bytes));
MOCK_METHOD1(AddForcedURL, void(const GURL& url));
MOCK_METHOD2(ShouldAcquirePageThumbnail,
bool(const GURL& url, ui::PageTransition transition));
// Implementation of RefcountedKeyedService.
void ShutdownOnUIThread() override {}
protected:
~MockThumbnailService() override = default;
};
// A helper class to wait until a navigation finishes and completes the first
// paint (as per WebContentsObserver::DidFirstVisuallyNonEmptyPaint). Similar
// to content::TestNavigationObserver, but waits for the paint in addition to
// the navigation (and is otherwise simplified).
// Note: This is a single-use class, supporting only a single navigation. Create
// a new instance for each navigation.
class NavigationAndFirstPaintWaiter : public content::WebContentsObserver {
public:
explicit NavigationAndFirstPaintWaiter(content::WebContents* web_contents)
: content::WebContentsObserver(web_contents) {}
~NavigationAndFirstPaintWaiter() override {}
void Wait() { run_loop_.Run(); }
private:
void DidFinishNavigation(
content::NavigationHandle* navigation_handle) override {
if (!navigation_handle->HasCommitted() ||
!navigation_handle->IsInMainFrame() ||
navigation_handle->IsSameDocument()) {
return;
}
CHECK(!did_finish_navigation_) << "Only a single navigation is supported";
did_finish_navigation_ = true;
}
void DidFirstVisuallyNonEmptyPaint() override {
if (did_finish_navigation_) {
run_loop_.Quit();
}
}
bool did_finish_navigation_ = false;
base::RunLoop run_loop_;
};
class ThumbnailTest : public InProcessBrowserTest {
public:
ThumbnailTest() {}
MockThumbnailService* thumbnail_service() {
return static_cast<MockThumbnailService*>(
ThumbnailServiceFactory::GetForProfile(browser()->profile()).get());
}
private:
void SetUp() override {
// Enabling pixel output is required for readback to work.
EnablePixelOutput();
InProcessBrowserTest::SetUp();
}
void SetUpCommandLine(base::CommandLine* command_line) override {
command_line->AppendSwitch(switches::kDisableGpu);
}
void SetUpInProcessBrowserTestFixture() override {
will_create_browser_context_services_subscription_ =
BrowserContextDependencyManager::GetInstance()
->RegisterWillCreateBrowserContextServicesCallbackForTesting(
base::Bind(&ThumbnailTest::OnWillCreateBrowserContextServices,
base::Unretained(this)));
}
static scoped_refptr<RefcountedKeyedService> CreateThumbnailService(
content::BrowserContext* context) {
return scoped_refptr<RefcountedKeyedService>(
new NiceMock<MockThumbnailService>());
}
void OnWillCreateBrowserContextServices(content::BrowserContext* context) {
ThumbnailServiceFactory::GetInstance()->SetTestingFactory(
context, base::BindRepeating(&ThumbnailTest::CreateThumbnailService));
}
std::unique_ptr<
base::CallbackList<void(content::BrowserContext*)>::Subscription>
will_create_browser_context_services_subscription_;
};
IN_PROC_BROWSER_TEST_F(ThumbnailTest,
ShouldCaptureOnNavigatingAwayExplicitWait) {
net::test_server::ControllableHttpResponse response_red(
embedded_test_server(), "/red.html");
net::test_server::ControllableHttpResponse response_yellow(
embedded_test_server(), "/yellow.html");
net::test_server::ControllableHttpResponse response_green(
embedded_test_server(), "/green.html");
ASSERT_TRUE(embedded_test_server()->Start());
const GURL about_blank_url("about:blank");
const GURL red_url = embedded_test_server()->GetURL("/red.html");
const GURL yellow_url = embedded_test_server()->GetURL("/yellow.html");
const GURL green_url = embedded_test_server()->GetURL("/green.html");
// Normally, ShouldAcquirePageThumbnail depends on many things, e.g. whether
// the given URL is in TopSites. For the purposes of this test, bypass all
// that and always take thumbnails, except for about:blank.
ON_CALL(*thumbnail_service(), ShouldAcquirePageThumbnail(about_blank_url, _))
.WillByDefault(Return(false));
ON_CALL(*thumbnail_service(), ShouldAcquirePageThumbnail(red_url, _))
.WillByDefault(Return(true));
ON_CALL(*thumbnail_service(), ShouldAcquirePageThumbnail(yellow_url, _))
.WillByDefault(Return(true));
ON_CALL(*thumbnail_service(), ShouldAcquirePageThumbnail(green_url, _))
.WillByDefault(Return(true));
// The test framework opens an about:blank tab by default.
content::WebContents* active_tab =
browser()->tab_strip_model()->GetActiveWebContents();
ASSERT_EQ(about_blank_url, active_tab->GetLastCommittedURL());
EXPECT_CALL(
*thumbnail_service(),
SetPageThumbnail(Field(&ThumbnailingContext::url, about_blank_url), _))
.Times(0);
{
// Navigate to the red page.
NavigationAndFirstPaintWaiter waiter(active_tab);
browser()->OpenURL(content::OpenURLParams(
red_url, content::Referrer(), WindowOpenDisposition::CURRENT_TAB,
ui::PAGE_TRANSITION_TYPED, false));
response_red.WaitForRequest();
response_red.Send(kHttpResponseHeader);
response_red.Send(MakeHtmlDocument("red"));
response_red.Done();
waiter.Wait();
}
{
// Before navigating away from the red page, we should take a thumbnail.
base::RunLoop run_loop;
EXPECT_CALL(*thumbnail_service(),
SetPageThumbnail(Field(&ThumbnailingContext::url, red_url),
ImageColorIs(SK_ColorRED)))
.WillOnce(DoAll(RunClosure(run_loop.QuitClosure()), Return(true)));
NavigationAndFirstPaintWaiter waiter(active_tab);
browser()->OpenURL(content::OpenURLParams(
yellow_url, content::Referrer(), WindowOpenDisposition::CURRENT_TAB,
ui::PAGE_TRANSITION_TYPED, false));
response_yellow.WaitForRequest();
// Note: It's important that we wait for the thumbnailing to finish *before*
// sending the response. Otherwise, the DocumentAvailableInMainFrame event
// might fire first, and we'll discard the captured thumbnail.
run_loop.Run();
response_yellow.Send(kHttpResponseHeader);
response_yellow.Send(MakeHtmlDocument("yellow"));
response_yellow.Done();
waiter.Wait();
}
{
// Before navigating away from the yellow page, we should take a thumbnail.
base::RunLoop run_loop;
EXPECT_CALL(*thumbnail_service(),
SetPageThumbnail(Field(&ThumbnailingContext::url, yellow_url),
ImageColorIs(SK_ColorYELLOW)))
.WillOnce(DoAll(RunClosure(run_loop.QuitClosure()), Return(true)));
NavigationAndFirstPaintWaiter waiter(active_tab);
browser()->OpenURL(content::OpenURLParams(
green_url, content::Referrer(), WindowOpenDisposition::CURRENT_TAB,
ui::PAGE_TRANSITION_TYPED, false));
response_green.WaitForRequest();
// As above, but send the response header before waiting for the
// thumbnailing to finish. This should still be safe.
response_green.Send(kHttpResponseHeader);
run_loop.Run();
response_green.Send(MakeHtmlDocument("green"));
response_green.Done();
waiter.Wait();
}
}
IN_PROC_BROWSER_TEST_F(ThumbnailTest,
ShouldCaptureOnNavigatingAwaySlowPageLoad) {
ASSERT_TRUE(embedded_test_server()->Start());
const GURL about_blank_url("about:blank");
const GURL red_url =
embedded_test_server()->GetURL("/thumbnail_capture/red.html");
const GURL slow_url = embedded_test_server()->GetURL("/slow?1");
// Normally, ShouldAcquirePageThumbnail depends on many things, e.g. whether
// the given URL is in TopSites. For the purposes of this test, bypass all
// that and just take thumbnails of the red page.
ON_CALL(*thumbnail_service(), ShouldAcquirePageThumbnail(about_blank_url, _))
.WillByDefault(Return(false));
ON_CALL(*thumbnail_service(), ShouldAcquirePageThumbnail(red_url, _))
.WillByDefault(Return(true));
ON_CALL(*thumbnail_service(), ShouldAcquirePageThumbnail(slow_url, _))
.WillByDefault(Return(false));
// The test framework opens an about:blank tab by default.
content::WebContents* active_tab =
browser()->tab_strip_model()->GetActiveWebContents();
ASSERT_EQ(about_blank_url, active_tab->GetLastCommittedURL());
EXPECT_CALL(
*thumbnail_service(),
SetPageThumbnail(Field(&ThumbnailingContext::url, about_blank_url), _))
.Times(0);
// Navigate to the red page.
{
NavigationAndFirstPaintWaiter waiter(
browser()->tab_strip_model()->GetActiveWebContents());
ui_test_utils::NavigateToURL(browser(), red_url);
waiter.Wait();
}
// Before navigating away from the red page, we should take a thumbnail.
// Note that the page load is deliberately slowed down, so that the
// thumbnailing process has time to finish before the new page comes in.
EXPECT_CALL(*thumbnail_service(),
SetPageThumbnail(Field(&ThumbnailingContext::url, red_url),
ImageColorIs(SK_ColorRED)))
.WillOnce(Return(true));
ui_test_utils::NavigateToURL(browser(), slow_url);
}
IN_PROC_BROWSER_TEST_F(ThumbnailTest,
ShouldContainProperContentIfCapturedOnNavigatingAway) {
ASSERT_TRUE(embedded_test_server()->Start());
const GURL about_blank_url("about:blank");
const GURL red_url =
embedded_test_server()->GetURL("/thumbnail_capture/red.html");
const GURL yellow_url =
embedded_test_server()->GetURL("/thumbnail_capture/yellow.html");
// Normally, ShouldAcquirePageThumbnail depends on many things, e.g. whether
// the given URL is in TopSites. For the purposes of this test, bypass all
// that and just take thumbnails of the red page.
ON_CALL(*thumbnail_service(), ShouldAcquirePageThumbnail(about_blank_url, _))
.WillByDefault(Return(false));
ON_CALL(*thumbnail_service(), ShouldAcquirePageThumbnail(red_url, _))
.WillByDefault(Return(true));
ON_CALL(*thumbnail_service(), ShouldAcquirePageThumbnail(yellow_url, _))
.WillByDefault(Return(false));
// The test framework opens an about:blank tab by default.
content::WebContents* active_tab =
browser()->tab_strip_model()->GetActiveWebContents();
ASSERT_EQ(about_blank_url, active_tab->GetLastCommittedURL());
EXPECT_CALL(
*thumbnail_service(),
SetPageThumbnail(Field(&ThumbnailingContext::url, about_blank_url), _))
.Times(0);
// Navigate to the red page.
ui_test_utils::NavigateToURL(browser(), red_url);
// Before navigating away from the red page, we should attempt to take a
// thumbnail, which might or might not succeed before we arrive at the new
// page. In any case, we must not get an image with wrong (non-red) contents.
EXPECT_CALL(*thumbnail_service(),
SetPageThumbnail(Field(&ThumbnailingContext::url, red_url),
ImageColorIs(SK_ColorRED)))
.Times(AtMost(1))
.WillOnce(Return(true));
ui_test_utils::NavigateToURL(browser(), yellow_url);
}
IN_PROC_BROWSER_TEST_F(ThumbnailTest,
ShouldContainProperContentIfCapturedOnTabSwitch) {
ASSERT_TRUE(embedded_test_server()->Start());
const GURL about_blank_url("about:blank");
const GURL red_url =
embedded_test_server()->GetURL("/thumbnail_capture/red.html");
const GURL yellow_url =
embedded_test_server()->GetURL("/thumbnail_capture/yellow.html");
// Normally, ShouldAcquirePageThumbnail depends on many things, e.g. whether
// the given URL is in TopSites. For the purposes of this test, bypass all
// that and just take thumbnails of the red and the yellow page.
ON_CALL(*thumbnail_service(), ShouldAcquirePageThumbnail(about_blank_url, _))
.WillByDefault(Return(false));
ON_CALL(*thumbnail_service(), ShouldAcquirePageThumbnail(red_url, _))
.WillByDefault(Return(true));
ON_CALL(*thumbnail_service(), ShouldAcquirePageThumbnail(yellow_url, _))
.WillByDefault(Return(true));
// The test framework opens an about:blank tab by default.
content::WebContents* active_tab =
browser()->tab_strip_model()->GetActiveWebContents();
ASSERT_EQ(about_blank_url, active_tab->GetLastCommittedURL());
// Open a new tab with the red page.
ui_test_utils::NavigateToURLWithDisposition(
browser(), red_url, WindowOpenDisposition::NEW_FOREGROUND_TAB,
ui_test_utils::BROWSER_TEST_WAIT_FOR_TAB |
ui_test_utils::BROWSER_TEST_WAIT_FOR_NAVIGATION);
active_tab = browser()->tab_strip_model()->GetActiveWebContents();
// Open another tab with the yellow page. Before the red tab gets hidden, we
// should attempt to take a thumbnail of it, which might or might not succeed.
// In any case, we must not get an image with wrong (non-red) contents.
EXPECT_CALL(*thumbnail_service(),
SetPageThumbnail(Field(&ThumbnailingContext::url, red_url),
ImageColorIs(SK_ColorRED)))
.Times(AtMost(1))
.WillOnce(Return(true));
ui_test_utils::NavigateToURLWithDisposition(
browser(), yellow_url, WindowOpenDisposition::NEW_FOREGROUND_TAB,
ui_test_utils::BROWSER_TEST_WAIT_FOR_TAB |
ui_test_utils::BROWSER_TEST_WAIT_FOR_NAVIGATION);
active_tab = browser()->tab_strip_model()->GetActiveWebContents();
// Switch back to the first tab. Again, we should attempt to take a thumbnail
// (of the yellow page this time), which might or might not succeed, but if it
// does must have the correct contents.
EXPECT_CALL(*thumbnail_service(),
SetPageThumbnail(Field(&ThumbnailingContext::url, yellow_url),
ImageColorIs(SK_ColorYELLOW)))
.Times(AtMost(1))
.WillOnce(Return(true));
ASSERT_EQ(2, browser()->tab_strip_model()->active_index());
ASSERT_EQ(yellow_url, active_tab->GetLastCommittedURL());
browser()->tab_strip_model()->ActivateTabAt(0, /*user_gesture=*/true);
active_tab = browser()->tab_strip_model()->GetActiveWebContents();
ASSERT_EQ(about_blank_url, active_tab->GetLastCommittedURL());
// Open another about:blank tab, to give the thumbnailing process above some
// time to finish (or fail). Without this, the test always finishes before
// the thumbnailing process does, so we'd have no chance to check the image
// contents.
ui_test_utils::NavigateToURLWithDisposition(
browser(), about_blank_url, WindowOpenDisposition::NEW_FOREGROUND_TAB,
ui_test_utils::BROWSER_TEST_WAIT_FOR_TAB |
ui_test_utils::BROWSER_TEST_WAIT_FOR_NAVIGATION);
}
} // namespace
} // namespace thumbnails
#endif // !defined(OS_CHROMEOS)