blob: 35f9eb84ece3da3763a832f7b834011739d413af [file] [log] [blame]
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <memory>
#include <string>
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/callback_helpers.h"
#include "base/json/values_util.h"
#include "base/memory/raw_ptr.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/time/time.h"
#include "chrome/browser/devtools/devtools_window_testing.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/hats/hats_service.h"
#include "chrome/browser/ui/hats/hats_service_factory.h"
#include "chrome/browser/ui/hats/mock_hats_service.h"
#include "chrome/browser/ui/test/test_browser_dialog.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/hats/hats_next_web_dialog.h"
#include "chrome/browser/ui/zoom/chrome_zoom_level_prefs.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/pref_names.h"
#include "chrome/test/base/scoped_browser_locale.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/version_info/version_info.h"
#include "content/public/test/browser_test.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "third_party/blink/public/common/page/page_zoom.h"
#include "url/gurl.h"
namespace {
// The product specific data expected by the test survey. The boolean values are
// checked in hats_next_mock.html.
const SurveyBitsData kHatsNextTestSurveyProductSpecificBitsData{
{"Test Field 1", true},
{"Test Field 2", false}};
const SurveyStringData kHatsNextTestSurveyProductSpecificStringData{
{"Test Field 3", "Test value"}};
// The locale expected by the test survey. This value is checked in
// hats_next_mock.html for tests that expect a loaded response.
const std::string kTestLocale = "lt";
} // namespace
class MockHatsNextWebDialog : public HatsNextWebDialog {
public:
MockHatsNextWebDialog(Browser* browser,
const std::string& trigger_id,
const GURL& hats_survey_url,
const base::TimeDelta& timeout,
base::OnceClosure success_callback,
base::OnceClosure failure_callback,
const SurveyBitsData& product_specific_bits_data,
const SurveyStringData& product_specific_string_data)
: HatsNextWebDialog(browser,
trigger_id,
hats_survey_url,
timeout,
std::move(success_callback),
std::move(failure_callback),
product_specific_bits_data,
product_specific_string_data) {}
MOCK_METHOD(void, ShowWidget, (), (override));
MOCK_METHOD(void, CloseWidget, (), (override));
MOCK_METHOD(void, UpdateWidgetSize, (), (override));
void WaitForClose() {
base::RunLoop run_loop;
EXPECT_CALL(*this, CloseWidget).WillOnce([&]() {
widget_->Close();
run_loop.Quit();
});
run_loop.Run();
}
void WaitForUpdateWidgetSize() {
base::RunLoop run_loop;
EXPECT_CALL(*this, UpdateWidgetSize).WillOnce(testing::Invoke([&run_loop] {
run_loop.Quit();
}));
run_loop.Run();
}
};
class HatsNextWebDialogBrowserTest : public InProcessBrowserTest {
public:
void SetUpOnMainThread() override {
hats_service_ = static_cast<MockHatsService*>(
HatsServiceFactory::GetInstance()->SetTestingFactoryAndUse(
browser()->profile(), base::BindRepeating(&BuildMockHatsService)));
}
// Open a blank tab in the main browser, inspect it, and return the devtools
// Browser for the undocked devtools window.
Browser* OpenUndockedDevToolsWindow() {
EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), GURL("about:blank")));
const bool is_docked = false;
DevToolsWindow* devtools_window =
DevToolsWindowTesting::OpenDevToolsWindowSync(browser(), is_docked);
return devtools_window->browser_;
}
MockHatsService* hats_service() { return hats_service_; }
base::OnceClosure GetSuccessClosure() {
return base::BindLambdaForTesting([&]() { ++success_count; });
}
base::OnceClosure GetFailureClosure() {
return base::BindLambdaForTesting([&]() { ++failure_count; });
}
int success_count = 0;
int failure_count = 0;
private:
raw_ptr<MockHatsService, DanglingUntriaged> hats_service_;
};
// Test that the web dialog correctly receives change to history state that
// indicates a survey is ready to be shown.
IN_PROC_BROWSER_TEST_F(HatsNextWebDialogBrowserTest, SurveyLoaded) {
ASSERT_TRUE(embedded_test_server()->Start());
// Use the preference path constants defined in hats_service.cc.
const std::string kLastSurveyStartedTime =
std::string(kHatsSurveyTriggerTesting) + ".last_survey_started_time";
const std::string kLastMajorVersion =
std::string(kHatsSurveyTriggerTesting) + ".last_major_version";
ScopedBrowserLocale browser_locale(kTestLocale);
auto* dialog = new MockHatsNextWebDialog(
browser(), kHatsNextSurveyTriggerIDTesting,
embedded_test_server()->GetURL("/hats/hats_next_mock.html"),
base::Seconds(100), GetSuccessClosure(), GetFailureClosure(),
kHatsNextTestSurveyProductSpecificBitsData,
kHatsNextTestSurveyProductSpecificStringData);
// Check that no record of a survey being shown is present.
{
const base::Value::Dict& pref_data =
browser()->profile()->GetPrefs()->GetDict(prefs::kHatsSurveyMetadata);
absl::optional<base::Time> last_survey_started_time =
base::ValueToTime(pref_data.FindByDottedPath(kLastSurveyStartedTime));
absl::optional<int> last_major_version =
pref_data.FindIntByDottedPath(kLastMajorVersion);
ASSERT_FALSE(last_survey_started_time.has_value());
ASSERT_FALSE(last_major_version.has_value());
}
// The hats_next_mock.html will provide a state update to the dialog to
// indicate that the survey has been loaded.
base::RunLoop run_loop;
EXPECT_CALL(*dialog, ShowWidget)
.WillOnce(testing::Invoke([dialog, &run_loop]() {
EXPECT_FALSE(dialog->IsWaitingForSurveyForTesting());
run_loop.Quit();
}));
run_loop.Run();
EXPECT_EQ(1, success_count);
EXPECT_EQ(0, failure_count);
// Check that a record of the survey being shown has been recorded.
{
const base::Value::Dict& pref_data =
browser()->profile()->GetPrefs()->GetDict(prefs::kHatsSurveyMetadata);
absl::optional<base::Time> last_survey_started_time =
base::ValueToTime(pref_data.FindByDottedPath(kLastSurveyStartedTime));
absl::optional<int> last_major_version =
pref_data.FindIntByDottedPath(kLastMajorVersion);
ASSERT_TRUE(last_survey_started_time.has_value());
ASSERT_TRUE(last_major_version.has_value());
ASSERT_EQ(static_cast<uint32_t>(*last_major_version),
version_info::GetVersion().components()[0]);
}
}
// Test that the web dialog correctly receives change to history state that
// indicates the survey window should be closed.
IN_PROC_BROWSER_TEST_F(HatsNextWebDialogBrowserTest, SurveyClosed) {
ASSERT_TRUE(embedded_test_server()->Start());
base::HistogramTester histogram_tester;
EXPECT_CALL(*hats_service(), HatsNextDialogClosed);
auto* dialog = new MockHatsNextWebDialog(
browser(), "close_for_testing",
embedded_test_server()->GetURL("/hats/hats_next_mock.html"),
base::Seconds(100), GetSuccessClosure(), GetFailureClosure(), {}, {});
// The hats_next_mock.html will provide a state update to the dialog to
// indicate that the survey window should be closed.
dialog->WaitForClose();
EXPECT_EQ(0, success_count);
EXPECT_EQ(1, failure_count);
// Because no loaded state was provided, only a rejection should be recorded.
histogram_tester.ExpectUniqueSample(
kHatsShouldShowSurveyReasonHistogram,
HatsService::ShouldShowSurveyReasons::kNoRejectedByHatsService, 1);
}
// Test that a survey which first reports as loaded, then reports closure, only
// logs that the survey was shown.
IN_PROC_BROWSER_TEST_F(HatsNextWebDialogBrowserTest, SurveyLoadedThenClosed) {
ASSERT_TRUE(embedded_test_server()->Start());
base::HistogramTester histogram_tester;
ScopedBrowserLocale browser_locale(kTestLocale);
EXPECT_CALL(*hats_service(), HatsNextDialogClosed);
auto* dialog = new MockHatsNextWebDialog(
browser(), kHatsNextSurveyTriggerIDTesting,
embedded_test_server()->GetURL("/hats/hats_next_mock.html"),
base::Seconds(100), GetSuccessClosure(), GetFailureClosure(),
kHatsNextTestSurveyProductSpecificBitsData,
kHatsNextTestSurveyProductSpecificStringData);
dialog->WaitForClose();
EXPECT_EQ(1, success_count);
EXPECT_EQ(0, failure_count);
// The only recorded sample should indicate that the survey was shown.
histogram_tester.ExpectUniqueSample(
kHatsShouldShowSurveyReasonHistogram,
HatsService::ShouldShowSurveyReasons::kYes, 1);
}
// Test that if the survey does not indicate it is ready for display before the
// timeout the widget is closed.
IN_PROC_BROWSER_TEST_F(HatsNextWebDialogBrowserTest, SurveyTimeout) {
ASSERT_TRUE(embedded_test_server()->Start());
base::HistogramTester histogram_tester;
EXPECT_CALL(*hats_service(), HatsNextDialogClosed);
auto* dialog = new MockHatsNextWebDialog(
browser(), "invalid_test",
embedded_test_server()->GetURL("/hats/non_existent.html"),
base::Milliseconds(1), GetSuccessClosure(), GetFailureClosure(), {}, {});
dialog->WaitForClose();
EXPECT_EQ(0, success_count);
EXPECT_EQ(1, failure_count);
histogram_tester.ExpectUniqueSample(
kHatsShouldShowSurveyReasonHistogram,
HatsService::ShouldShowSurveyReasons::kNoSurveyUnreachable, 1);
}
IN_PROC_BROWSER_TEST_F(HatsNextWebDialogBrowserTest, UnknownURLFragment) {
ASSERT_TRUE(embedded_test_server()->Start());
// Check that providing an unknown URL fragment results in the dialog being
// closed.
EXPECT_CALL(*hats_service(), HatsNextDialogClosed);
auto* dialog = new MockHatsNextWebDialog(
browser(), "invalid_url_fragment_for_testing",
embedded_test_server()->GetURL("/hats/hats_next_mock.html"),
base::Seconds(100), GetSuccessClosure(), GetFailureClosure(), {}, {});
dialog->WaitForClose();
EXPECT_EQ(0, success_count);
EXPECT_EQ(1, failure_count);
}
IN_PROC_BROWSER_TEST_F(HatsNextWebDialogBrowserTest, NewWebContents) {
ASSERT_TRUE(embedded_test_server()->Start());
auto* dialog = new MockHatsNextWebDialog(
browser(), "open_new_web_contents_for_testing",
embedded_test_server()->GetURL("/hats/hats_next_mock.html"),
base::Seconds(100), base::DoNothing(), base::DoNothing(), {}, {});
// The mock hats dialog will push a close state after it has attempted to
// open another web contents.
EXPECT_CALL(*hats_service(), HatsNextDialogClosed);
dialog->WaitForClose();
// Check that a tab with http://foo.com (defined in hats_next_mock.html) has
// been opened in the regular browser and is active.
EXPECT_EQ(
GURL("http://foo.com"),
browser()->tab_strip_model()->GetActiveWebContents()->GetVisibleURL());
}
// The devtools browser for undocked devtools has no tab strip and can't open
// new tabs. Instead it should open new WebContents in the main browser.
IN_PROC_BROWSER_TEST_F(HatsNextWebDialogBrowserTest,
NewWebContentsForDevtoolsBrowser) {
ASSERT_TRUE(embedded_test_server()->Start());
Browser* devtools_browser = OpenUndockedDevToolsWindow();
auto* dialog = new MockHatsNextWebDialog(
devtools_browser, "open_new_web_contents_for_testing",
embedded_test_server()->GetURL("/hats/hats_next_mock.html"),
base::Seconds(100), base::DoNothing(), base::DoNothing(), {}, {});
// The mock hats dialog will push a close state after it has attempted to
// open another web contents.
EXPECT_CALL(*hats_service(), HatsNextDialogClosed);
dialog->WaitForClose();
// Check that a tab with http://foo.com (defined in hats_next_mock.html) has
// been opened in the regular browser and is active.
EXPECT_EQ(
GURL("http://foo.com"),
browser()->tab_strip_model()->GetActiveWebContents()->GetVisibleURL());
}
IN_PROC_BROWSER_TEST_F(HatsNextWebDialogBrowserTest, DialogResize) {
ASSERT_TRUE(embedded_test_server()->Start());
auto* dialog = new MockHatsNextWebDialog(
browser(), "resize_for_testing",
embedded_test_server()->GetURL("/hats/hats_next_mock.html"),
base::Seconds(100), base::DoNothing(), base::DoNothing(), {}, {});
// Check that the dialog reports a preferred size the same as the size defined
// in hats_next_mock.html.
constexpr auto kTargetSize = gfx::Size(70, 300);
// Depending on renderer warm-up, an initial empty size may additionally be
// reported before hats_next_mock.html has had a chance to resize.
dialog->WaitForUpdateWidgetSize();
auto size = dialog->CalculatePreferredSize();
EXPECT_TRUE(size == kTargetSize || size == dialog->kMinSize);
if (size != kTargetSize) {
dialog->WaitForUpdateWidgetSize();
EXPECT_EQ(kTargetSize, dialog->CalculatePreferredSize());
}
}
IN_PROC_BROWSER_TEST_F(HatsNextWebDialogBrowserTest, MaximumSize) {
ASSERT_TRUE(embedded_test_server()->Start());
EXPECT_CALL(*hats_service(), HatsNextDialogClosed);
auto* dialog = new MockHatsNextWebDialog(
browser(), "resize_to_large_for_testing",
embedded_test_server()->GetURL("/hats/hats_next_mock.html"),
base::Seconds(100), base::DoNothing(), base::DoNothing(), {}, {});
// Check that the maximum size of the dialog is bounded appropriately by the
// dialogs maximum size. Depending on renderer warm-up, an initial empty size
// may additionally be reported before hats_next_mock.html has had a chance
// to resize.
dialog->WaitForUpdateWidgetSize();
auto size = dialog->CalculatePreferredSize();
EXPECT_TRUE(size == HatsNextWebDialog::kMaxSize || size == dialog->kMinSize);
if (size != HatsNextWebDialog::kMaxSize) {
dialog->WaitForUpdateWidgetSize();
EXPECT_EQ(HatsNextWebDialog::kMaxSize, dialog->CalculatePreferredSize());
}
}
IN_PROC_BROWSER_TEST_F(HatsNextWebDialogBrowserTest, ZoomLevel) {
// Ensure that the dialog correctly resets the zoom level to default.
browser()->profile()->GetZoomLevelPrefs()->SetDefaultZoomLevelPref(
blink::PageZoomFactorToZoomLevel(5.0f));
ScopedBrowserLocale browser_locale(kTestLocale);
ASSERT_TRUE(embedded_test_server()->Start());
auto* dialog = new MockHatsNextWebDialog(
browser(), kHatsNextSurveyTriggerIDTesting,
embedded_test_server()->GetURL("/hats/hats_next_mock.html"),
base::Seconds(100), GetSuccessClosure(), GetFailureClosure(),
kHatsNextTestSurveyProductSpecificBitsData,
kHatsNextTestSurveyProductSpecificStringData);
// Allow the dialog to open before checking the zoom level of the contents.
base::RunLoop run_loop;
EXPECT_CALL(*dialog, ShowWidget).WillOnce(testing::Invoke([&run_loop]() {
run_loop.Quit();
}));
run_loop.Run();
EXPECT_TRUE(blink::PageZoomValuesEqual(
content::HostZoomMap::GetDefaultForBrowserContext(dialog->otr_profile_)
->GetZoomLevel(dialog->web_view_->GetWebContents()),
blink::PageZoomFactorToZoomLevel(1.0f)));
}