| // Copyright 2014 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 "chrome/browser/ui/views/extensions/extension_install_dialog_view.h" |
| |
| #include <string> |
| #include <utility> |
| |
| #include "base/bind_helpers.h" |
| #include "base/files/file_path.h" |
| #include "base/macros.h" |
| #include "base/path_service.h" |
| #include "base/run_loop.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/extensions/chrome_test_extension_loader.h" |
| #include "chrome/browser/extensions/extension_browsertest.h" |
| #include "chrome/browser/extensions/extension_icon_manager.h" |
| #include "chrome/browser/extensions/extension_install_prompt.h" |
| #include "chrome/browser/extensions/extension_install_prompt_test_helper.h" |
| #include "chrome/browser/platform_util.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_window.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "chrome/browser/ui/test/test_browser_dialog.h" |
| #include "chrome/common/chrome_paths.h" |
| #include "chrome/common/extensions/extension_test_util.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "components/constrained_window/constrained_window_views.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/test/test_utils.h" |
| #include "extensions/common/extension.h" |
| #include "extensions/common/permissions/permission_message_provider.h" |
| #include "extensions/common/permissions/permissions_data.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/views/bubble/bubble_frame_view.h" |
| #include "ui/views/controls/scroll_view.h" |
| #include "ui/views/view.h" |
| #include "ui/views/widget/widget.h" |
| #include "ui/views/window/dialog_client_view.h" |
| |
| using extensions::PermissionIDSet; |
| using extensions::PermissionMessage; |
| using extensions::PermissionMessages; |
| |
| class ExtensionInstallDialogViewTestBase |
| : public extensions::ExtensionBrowserTest { |
| protected: |
| ExtensionInstallDialogViewTestBase(); |
| |
| void SetUpOnMainThread() override; |
| |
| // Creates and returns an install prompt of |prompt_type|. |
| std::unique_ptr<ExtensionInstallPrompt::Prompt> CreatePrompt( |
| ExtensionInstallPrompt::PromptType prompt_type); |
| |
| content::WebContents* web_contents() { return web_contents_; } |
| |
| private: |
| const extensions::Extension* extension_; |
| content::WebContents* web_contents_; |
| |
| DISALLOW_COPY_AND_ASSIGN(ExtensionInstallDialogViewTestBase); |
| }; |
| |
| ExtensionInstallDialogViewTestBase::ExtensionInstallDialogViewTestBase() |
| : extension_(nullptr), web_contents_(nullptr) {} |
| |
| void ExtensionInstallDialogViewTestBase::SetUpOnMainThread() { |
| extensions::ExtensionBrowserTest::SetUpOnMainThread(); |
| |
| extension_ = LoadExtension(test_data_dir_.AppendASCII( |
| "install_prompt/permissions_scrollbar_regression")); |
| |
| web_contents_ = browser()->tab_strip_model()->GetWebContentsAt(0); |
| } |
| |
| std::unique_ptr<ExtensionInstallPrompt::Prompt> |
| ExtensionInstallDialogViewTestBase::CreatePrompt( |
| ExtensionInstallPrompt::PromptType prompt_type) { |
| std::unique_ptr<ExtensionInstallPrompt::Prompt> prompt( |
| new ExtensionInstallPrompt::Prompt(prompt_type)); |
| prompt->set_extension(extension_); |
| |
| std::unique_ptr<ExtensionIconManager> icon_manager( |
| new ExtensionIconManager()); |
| prompt->set_icon(icon_manager->GetIcon(extension_->id())); |
| |
| return prompt; |
| } |
| |
| class ScrollbarTest : public ExtensionInstallDialogViewTestBase { |
| protected: |
| ScrollbarTest() {} |
| |
| bool IsScrollbarVisible( |
| std::unique_ptr<ExtensionInstallPrompt::Prompt> prompt); |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(ScrollbarTest); |
| }; |
| |
| bool ScrollbarTest::IsScrollbarVisible( |
| std::unique_ptr<ExtensionInstallPrompt::Prompt> prompt) { |
| ExtensionInstallDialogView* dialog = new ExtensionInstallDialogView( |
| profile(), web_contents(), ExtensionInstallPrompt::DoneCallback(), |
| std::move(prompt)); |
| |
| // Create the modal view around the install dialog view. |
| views::Widget* modal = constrained_window::CreateBrowserModalDialogViews( |
| dialog, web_contents()->GetTopLevelNativeWindow()); |
| modal->Show(); |
| content::RunAllTasksUntilIdle(); |
| |
| // Check if the vertical scrollbar is visible. |
| return dialog->scroll_view()->vertical_scroll_bar()->visible(); |
| } |
| |
| // Tests that a scrollbar _is_ shown for an excessively long extension |
| // install prompt. |
| IN_PROC_BROWSER_TEST_F(ScrollbarTest, LongPromptScrollbar) { |
| base::string16 permission_string(base::ASCIIToUTF16("Test")); |
| PermissionMessages permissions; |
| for (int i = 0; i < 20; i++) { |
| permissions.push_back(PermissionMessage(permission_string, |
| PermissionIDSet())); |
| } |
| std::unique_ptr<ExtensionInstallPrompt::Prompt> prompt = |
| CreatePrompt(ExtensionInstallPrompt::PERMISSIONS_PROMPT); |
| prompt->AddPermissions(permissions); |
| ASSERT_TRUE(IsScrollbarVisible(std::move(prompt))) |
| << "Scrollbar is not visible"; |
| } |
| |
| // Tests that a scrollbar isn't shown for this regression case. |
| // See crbug.com/385570 for details. |
| IN_PROC_BROWSER_TEST_F(ScrollbarTest, ScrollbarRegression) { |
| base::string16 permission_string(base::ASCIIToUTF16( |
| "Read and modify your data on *.facebook.com")); |
| PermissionMessages permissions; |
| permissions.push_back(PermissionMessage(permission_string, |
| PermissionIDSet())); |
| std::unique_ptr<ExtensionInstallPrompt::Prompt> prompt = |
| CreatePrompt(ExtensionInstallPrompt::PERMISSIONS_PROMPT); |
| prompt->AddPermissions(permissions); |
| ASSERT_FALSE(IsScrollbarVisible(std::move(prompt))) << "Scrollbar is visible"; |
| } |
| |
| class ExtensionInstallDialogViewTest |
| : public ExtensionInstallDialogViewTestBase { |
| protected: |
| ExtensionInstallDialogViewTest() {} |
| |
| views::DialogDelegateView* CreateAndShowPrompt( |
| ExtensionInstallPromptTestHelper* helper) { |
| std::unique_ptr<ExtensionInstallDialogView> dialog( |
| new ExtensionInstallDialogView( |
| profile(), web_contents(), helper->GetCallback(), |
| CreatePrompt(ExtensionInstallPrompt::INSTALL_PROMPT))); |
| views::DialogDelegateView* delegate_view = dialog.get(); |
| |
| views::Widget* modal_dialog = views::DialogDelegate::CreateDialogWidget( |
| dialog.release(), nullptr, |
| platform_util::GetViewForWindow( |
| browser()->window()->GetNativeWindow())); |
| modal_dialog->Show(); |
| |
| return delegate_view; |
| } |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(ExtensionInstallDialogViewTest); |
| }; |
| |
| // Verifies that the delegate is notified when the user selects to accept or |
| // cancel the install. |
| // |
| // Crashes flakily on Mac. See http://crbug.com/851167 |
| #if defined(OS_MACOSX) |
| #define MAYBE_NotifyDelegate DISABLED_NotifyDelegate |
| #else |
| #define MAYBE_NotifyDelegate NotifyDelegate |
| #endif |
| IN_PROC_BROWSER_TEST_F(ExtensionInstallDialogViewTest, NotifyDelegate) { |
| { |
| // User presses install. |
| ExtensionInstallPromptTestHelper helper; |
| views::DialogDelegateView* delegate_view = CreateAndShowPrompt(&helper); |
| delegate_view->GetDialogClientView()->AcceptWindow(); |
| EXPECT_EQ(ExtensionInstallPrompt::Result::ACCEPTED, helper.result()); |
| } |
| { |
| // User presses cancel. |
| ExtensionInstallPromptTestHelper helper; |
| views::DialogDelegateView* delegate_view = CreateAndShowPrompt(&helper); |
| delegate_view->GetDialogClientView()->CancelWindow(); |
| EXPECT_EQ(ExtensionInstallPrompt::Result::USER_CANCELED, helper.result()); |
| } |
| { |
| // Dialog is closed without the user explicitly choosing to proceed or |
| // cancel. |
| ExtensionInstallPromptTestHelper helper; |
| views::DialogDelegateView* delegate_view = CreateAndShowPrompt(&helper); |
| delegate_view->GetWidget()->Close(); |
| // TODO(devlin): Should this be ABORTED? |
| EXPECT_EQ(ExtensionInstallPrompt::Result::USER_CANCELED, helper.result()); |
| } |
| } |
| |
| // Verifies that the "Add extension" button is disabled initally, but re-enabled |
| // after a short time delay. |
| IN_PROC_BROWSER_TEST_F(ExtensionInstallDialogViewTest, InstallButtonDelay) { |
| ExtensionInstallDialogView::SetInstallButtonDelayForTesting(0); |
| ExtensionInstallPromptTestHelper helper; |
| views::DialogDelegateView* delegate_view = CreateAndShowPrompt(&helper); |
| |
| // Check that dialog is visible. |
| EXPECT_TRUE(delegate_view->visible()); |
| |
| // Check initial button states. |
| EXPECT_FALSE(delegate_view->IsDialogButtonEnabled(ui::DIALOG_BUTTON_OK)); |
| EXPECT_TRUE(delegate_view->IsDialogButtonEnabled(ui::DIALOG_BUTTON_CANCEL)); |
| EXPECT_TRUE(delegate_view->GetInitiallyFocusedView()->HasFocus()); |
| |
| // Check OK button state after timeout to verify that it is re-enabled. |
| base::RunLoop().RunUntilIdle(); |
| EXPECT_TRUE(delegate_view->IsDialogButtonEnabled(ui::DIALOG_BUTTON_OK)); |
| |
| // Ensure default button (cancel) has focus. |
| EXPECT_TRUE(delegate_view->GetInitiallyFocusedView()->HasFocus()); |
| delegate_view->Close(); |
| } |
| |
| class ExtensionInstallDialogViewInteractiveBrowserTest |
| : public DialogBrowserTest { |
| public: |
| ExtensionInstallDialogViewInteractiveBrowserTest() {} |
| |
| // DialogBrowserTest: |
| void ShowUi(const std::string& name) override { |
| extensions::ChromeTestExtensionLoader loader(browser()->profile()); |
| base::FilePath test_data_dir; |
| base::PathService::Get(chrome::DIR_TEST_DATA, &test_data_dir); |
| scoped_refptr<const extensions::Extension> extension = loader.LoadExtension( |
| test_data_dir.AppendASCII("extensions/uitest/long_name")); |
| |
| SkBitmap icon; |
| // The dialog will downscale large images. |
| icon.allocN32Pixels(800, 800); |
| icon.eraseARGB(255, 128, 255, 128); |
| |
| auto prompt = std::make_unique<ExtensionInstallPrompt::Prompt>(type_); |
| prompt->AddPermissions(permissions_); |
| prompt->set_retained_files(retained_files_); |
| prompt->set_retained_device_messages(retained_devices_); |
| |
| if (from_webstore_) |
| prompt->SetWebstoreData("69,420", true, 2.5, 37); |
| |
| auto* web_contents = browser()->tab_strip_model()->GetActiveWebContents(); |
| auto install_prompt = |
| std::make_unique<ExtensionInstallPrompt>(web_contents); |
| install_prompt->ShowDialog(base::DoNothing(), extension.get(), &icon, |
| std::move(prompt), |
| ExtensionInstallPrompt::ShowDialogCallback()); |
| } |
| |
| void set_from_webstore() { from_webstore_ = true; } |
| |
| void set_type(ExtensionInstallPrompt::PromptType type) { type_ = type; } |
| |
| void AddPermission(std::string permission) { |
| permissions_.push_back( |
| PermissionMessage(base::ASCIIToUTF16(permission), PermissionIDSet())); |
| } |
| |
| void AddRetainedFile(const base::FilePath& path) { |
| retained_files_.push_back(path); |
| } |
| |
| void AddRetainedDevice(const std::string& device) { |
| retained_devices_.push_back(base::ASCIIToUTF16(device)); |
| } |
| |
| void AddPermissionWithDetails( |
| std::string main_permission, |
| std::vector<base::string16> detailed_permissions) { |
| permissions_.push_back( |
| PermissionMessage(base::ASCIIToUTF16(main_permission), |
| PermissionIDSet(), std::move(detailed_permissions))); |
| } |
| |
| private: |
| ExtensionInstallPrompt::PromptType type_ = |
| ExtensionInstallPrompt::INSTALL_PROMPT; |
| bool from_webstore_ = false; |
| PermissionMessages permissions_; |
| std::vector<base::FilePath> retained_files_; |
| std::vector<base::string16> retained_devices_; |
| |
| DISALLOW_COPY_AND_ASSIGN(ExtensionInstallDialogViewInteractiveBrowserTest); |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionInstallDialogViewInteractiveBrowserTest, |
| InvokeUi_Simple) { |
| ShowAndVerifyUi(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionInstallDialogViewInteractiveBrowserTest, |
| InvokeUi_External) { |
| set_type(ExtensionInstallPrompt::EXTERNAL_INSTALL_PROMPT); |
| ShowAndVerifyUi(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionInstallDialogViewInteractiveBrowserTest, |
| InvokeUi_ExternalWithPermission) { |
| set_type(ExtensionInstallPrompt::EXTERNAL_INSTALL_PROMPT); |
| AddPermission("Example permission"); |
| ShowAndVerifyUi(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionInstallDialogViewInteractiveBrowserTest, |
| InvokeUi_ReEnable) { |
| set_type(ExtensionInstallPrompt::RE_ENABLE_PROMPT); |
| AddPermission("Example permission"); |
| ShowAndVerifyUi(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionInstallDialogViewInteractiveBrowserTest, |
| InvokeUi_FromWebstore) { |
| set_type(ExtensionInstallPrompt::WEBSTORE_WIDGET_PROMPT); |
| set_from_webstore(); |
| ShowAndVerifyUi(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionInstallDialogViewInteractiveBrowserTest, |
| InvokeUi_FromWebstoreWithPermission) { |
| set_type(ExtensionInstallPrompt::WEBSTORE_WIDGET_PROMPT); |
| set_from_webstore(); |
| AddPermission("Example permission"); |
| ShowAndVerifyUi(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionInstallDialogViewInteractiveBrowserTest, |
| InvokeUi_MultilinePermission) { |
| AddPermission( |
| "In the shade of the house, in the sunshine of the riverbank " |
| "near the boats, in the shade of the Sal-wood forest, in the " |
| "shade of the fig tree is where Siddhartha grew up"); |
| ShowAndVerifyUi(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionInstallDialogViewInteractiveBrowserTest, |
| InvokeUi_ManyPermissions) { |
| for (int i = 0; i < 20; i++) |
| AddPermission("Example permission"); |
| ShowAndVerifyUi(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionInstallDialogViewInteractiveBrowserTest, |
| InvokeUi_DetailedPermission) { |
| AddPermissionWithDetails( |
| "Example header permission", |
| {base::ASCIIToUTF16("Detailed permission 1"), |
| base::ASCIIToUTF16("Detailed permission 2"), |
| base::ASCIIToUTF16("Very very very very very very long detailed " |
| "permission that wraps to a new line")}); |
| ShowAndVerifyUi(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionInstallDialogViewInteractiveBrowserTest, |
| InvokeUi_WithRetainedFiles) { |
| AddRetainedFile(base::FilePath(FILE_PATH_LITERAL("/dev/null"))); |
| AddRetainedFile(base::FilePath(FILE_PATH_LITERAL("/dev/zero"))); |
| AddRetainedFile(base::FilePath(FILE_PATH_LITERAL("/dev/random"))); |
| AddRetainedFile(base::FilePath(FILE_PATH_LITERAL( |
| "/some/very/very/very/very/very/long/path/longer/than/the/" |
| "line/length/file_with_long_name_too.txt"))); |
| ShowAndVerifyUi(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionInstallDialogViewInteractiveBrowserTest, |
| InvokeUi_WithRetainedDevices) { |
| AddRetainedDevice("USB Device"); |
| AddRetainedDevice("USB Device With Longer Name"); |
| AddRetainedDevice( |
| "Another USB Device With A Very Very Very Very Very Very " |
| "Long Name So That It Hopefully Wraps to A New Line"); |
| ShowAndVerifyUi(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionInstallDialogViewInteractiveBrowserTest, |
| InvokeUi_AllInfoTypes) { |
| AddPermission("Example permission"); |
| AddPermissionWithDetails("This permission has details", |
| {base::ASCIIToUTF16("Detailed permission 1"), |
| base::ASCIIToUTF16("Detailed permission 2")}); |
| AddRetainedDevice("USB Device"); |
| AddRetainedFile(base::FilePath(FILE_PATH_LITERAL("/dev/null"))); |
| ShowAndVerifyUi(); |
| } |
| |
| class ExtensionInstallDialogRatingsSectionTest |
| : public ExtensionInstallDialogViewTest { |
| public: |
| ExtensionInstallDialogRatingsSectionTest() {} |
| |
| void TestRatingsSectionA11y(int num_ratings, |
| double average_rating, |
| const std::string& expected_text); |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(ExtensionInstallDialogRatingsSectionTest); |
| }; |
| |
| void ExtensionInstallDialogRatingsSectionTest::TestRatingsSectionA11y( |
| int num_ratings, |
| double average_rating, |
| const std::string& expected_text) { |
| SCOPED_TRACE(base::StringPrintf( |
| "Testing with %d ratings, %f average rating. Expected text: '%s'.", |
| num_ratings, average_rating, expected_text.c_str())); |
| std::unique_ptr<ExtensionInstallPrompt::Prompt> prompt = |
| CreatePrompt(ExtensionInstallPrompt::REPAIR_PROMPT); |
| prompt->SetWebstoreData("1,234", true, average_rating, num_ratings); |
| |
| ExtensionInstallDialogView* dialog = new ExtensionInstallDialogView( |
| profile(), web_contents(), base::DoNothing(), std::move(prompt)); |
| |
| views::Widget* modal_dialog = views::DialogDelegate::CreateDialogWidget( |
| dialog, nullptr, |
| platform_util::GetViewForWindow(browser()->window()->GetNativeWindow())); |
| modal_dialog->Show(); |
| |
| views::View* rating_view = modal_dialog->non_client_view()->GetViewByID( |
| ExtensionInstallDialogView::kRatingsViewId); |
| ASSERT_TRUE(rating_view); |
| { |
| ui::AXNodeData node_data; |
| rating_view->GetAccessibleNodeData(&node_data); |
| EXPECT_EQ(ax::mojom::Role::kStaticText, node_data.role); |
| EXPECT_EQ(expected_text, |
| node_data.GetStringAttribute(ax::mojom::StringAttribute::kName)); |
| } |
| |
| for (int i = 0; i < rating_view->child_count(); ++i) { |
| views::View* child = rating_view->child_at(i); |
| ui::AXNodeData node_data; |
| child->GetAccessibleNodeData(&node_data); |
| EXPECT_EQ(ax::mojom::Role::kIgnored, node_data.role); |
| } |
| |
| modal_dialog->Close(); |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionInstallDialogRatingsSectionTest, |
| RatingsSectionA11y) { |
| TestRatingsSectionA11y(400, 3.297, "Rated 3.3 by 400 users."); |
| TestRatingsSectionA11y(1, 1.0, "Rated 1.0 by one user."); |
| TestRatingsSectionA11y(0, 0.0, "Not yet rated by any users."); |
| } |