| // 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 "base/run_loop.h" |
| #include "base/test/run_until.h" |
| #include "base/test/with_feature_override.h" |
| #include "build/build_config.h" |
| #include "build/buildflag.h" |
| #include "chrome/app/chrome_command_ids.h" |
| #include "chrome/browser/extensions/extension_apitest.h" |
| #include "chrome/browser/pdf/pdf_extension_test_base.h" |
| #include "chrome/browser/pdf/pdf_extension_test_util.h" |
| #include "chrome/browser/renderer_context_menu/render_view_context_menu_browsertest_util.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model_observer.h" |
| #include "chrome/test/base/interactive_test_utils.h" |
| #include "chrome/test/base/ui_test_utils.h" |
| #include "components/pdf/browser/pdf_frame_util.h" |
| #include "content/public/browser/browser_plugin_guest_manager.h" |
| #include "content/public/browser/focused_node_details.h" |
| #include "content/public/browser/render_widget_host_view.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "content/public/test/focus_changed_observer.h" |
| #include "content/public/test/hit_test_region_observer.h" |
| #include "content/public/test/test_utils.h" |
| #include "extensions/browser/api/extensions_api_client.h" |
| #include "extensions/browser/guest_view/mime_handler_view/mime_handler_view_guest.h" |
| #include "net/dns/mock_host_resolver.h" |
| #include "net/test/embedded_test_server/embedded_test_server.h" |
| #include "pdf/pdf_features.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/public/mojom/input/focus_type.mojom-shared.h" |
| #include "ui/events/keycodes/keyboard_codes.h" |
| #include "url/gurl.h" |
| |
| #if defined(TOOLKIT_VIEWS) && defined(USE_AURA) |
| #include "content/public/browser/touch_selection_controller_client_manager.h" |
| #include "ui/events/base_event_utils.h" |
| #include "ui/events/event.h" |
| #include "ui/events/gesture_event_details.h" |
| #include "ui/events/types/event_type.h" |
| #include "ui/gfx/geometry/point.h" |
| #include "ui/gfx/geometry/point_f.h" |
| #include "ui/gfx/geometry/test/geometry_util.h" |
| #include "ui/gfx/selection_bound.h" |
| #include "ui/touch_selection/touch_selection_controller.h" |
| #include "ui/views/touchui/touch_selection_menu_views.h" |
| #include "ui/views/widget/any_widget_observer.h" |
| #include "ui/views/widget/widget.h" |
| #endif // defined(TOOLKIT_VIEWS) && defined(USE_AURA) |
| |
| namespace { |
| |
| using ::pdf_extension_test_util::ConvertPageCoordToScreenCoord; |
| using ::pdf_extension_test_util::SetInputFocusOnPlugin; |
| |
| class PDFExtensionInteractiveUITest : public base::test::WithFeatureOverride, |
| public PDFExtensionTestBase { |
| public: |
| PDFExtensionInteractiveUITest() |
| : base::test::WithFeatureOverride(chrome_pdf::features::kPdfOopif) {} |
| |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| PDFExtensionTestBase::SetUpCommandLine(command_line); |
| |
| content::IsolateAllSitesForTesting(command_line); |
| } |
| |
| content::FocusedNodeDetails TabAndWait(content::WebContents* web_contents, |
| bool forward) { |
| content::FocusChangedObserver focus_observer(web_contents); |
| if (!ui_test_utils::SendKeyPressSync(browser(), ui::VKEY_TAB, |
| /*control=*/false, |
| /*shift=*/!forward, |
| /*alt=*/false, |
| /*command=*/false)) { |
| ADD_FAILURE() << "Failed to send key press"; |
| return {}; |
| } |
| return focus_observer.Wait(); |
| } |
| |
| bool UseOopif() const override { return GetParam(); } |
| }; |
| |
| class TabChangedWaiter : public TabStripModelObserver { |
| public: |
| explicit TabChangedWaiter(Browser* browser) { |
| browser->tab_strip_model()->AddObserver(this); |
| } |
| TabChangedWaiter(const TabChangedWaiter&) = delete; |
| TabChangedWaiter& operator=(const TabChangedWaiter&) = delete; |
| ~TabChangedWaiter() override = default; |
| |
| void Wait() { run_loop_.Run(); } |
| |
| // TabStripModelObserver: |
| void OnTabStripModelChanged( |
| TabStripModel* tab_strip_model, |
| const TabStripModelChange& change, |
| const TabStripSelectionChange& selection) override { |
| if (change.type() == TabStripModelChange::kSelectionOnly) |
| run_loop_.Quit(); |
| } |
| |
| private: |
| base::RunLoop run_loop_; |
| }; |
| |
| } // namespace |
| |
| // TODO(crbug.com/333802743): re-enable the test |
| // For crbug.com/1038918 |
| IN_PROC_BROWSER_TEST_P(PDFExtensionInteractiveUITest, |
| DISABLED_CtrlPageUpDownSwitchesTabs) { |
| content::RenderFrameHost* extension_host = LoadPdfInNewTabGetExtensionHost( |
| embedded_test_server()->GetURL("/pdf/test.pdf")); |
| ASSERT_TRUE(extension_host); |
| |
| auto* tab_strip_model = browser()->tab_strip_model(); |
| ASSERT_EQ(2, tab_strip_model->count()); |
| EXPECT_EQ(1, tab_strip_model->active_index()); |
| |
| SetInputFocusOnPlugin(extension_host, GetEmbedderWebContents()); |
| |
| { |
| TabChangedWaiter tab_changed_waiter(browser()); |
| ASSERT_TRUE(ui_test_utils::SendKeyPressSync(browser(), ui::VKEY_NEXT, |
| /*control=*/true, |
| /*shift=*/false, |
| /*alt=*/false, |
| /*command=*/false)); |
| tab_changed_waiter.Wait(); |
| } |
| ASSERT_EQ(2, tab_strip_model->count()); |
| EXPECT_EQ(0, tab_strip_model->active_index()); |
| |
| { |
| TabChangedWaiter tab_changed_waiter(browser()); |
| ASSERT_TRUE(ui_test_utils::SendKeyPressSync(browser(), ui::VKEY_PRIOR, |
| /*control=*/true, |
| /*shift=*/false, |
| /*alt=*/false, |
| /*command=*/false)); |
| tab_changed_waiter.Wait(); |
| } |
| ASSERT_EQ(2, tab_strip_model->count()); |
| EXPECT_EQ(1, tab_strip_model->active_index()); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(PDFExtensionInteractiveUITest, FocusForwardTraversal) { |
| content::RenderFrameHost* extension_host = LoadPdfInNewTabGetExtensionHost( |
| embedded_test_server()->GetURL("/pdf/test.pdf#toolbar=0")); |
| ASSERT_TRUE(extension_host); |
| auto* target_web_contents = |
| content::WebContents::FromRenderFrameHost(extension_host); |
| |
| // Tab in. |
| content::FocusedNodeDetails details = |
| TabAndWait(target_web_contents, /*forward=*/true); |
| EXPECT_EQ(blink::mojom::FocusType::kForward, details.focus_type); |
| |
| // Tab out. |
| details = TabAndWait(target_web_contents, /*forward=*/true); |
| EXPECT_EQ(blink::mojom::FocusType::kNone, details.focus_type); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(PDFExtensionInteractiveUITest, FocusReverseTraversal) { |
| content::RenderFrameHost* extension_host = LoadPdfInNewTabGetExtensionHost( |
| embedded_test_server()->GetURL("/pdf/test.pdf#toolbar=0")); |
| ASSERT_TRUE(extension_host); |
| auto* target_web_contents = |
| content::WebContents::FromRenderFrameHost(extension_host); |
| |
| // Tab in. |
| content::FocusedNodeDetails details = |
| TabAndWait(target_web_contents, /*forward=*/false); |
| EXPECT_EQ(blink::mojom::FocusType::kBackward, details.focus_type); |
| |
| // Tab out. |
| details = TabAndWait(target_web_contents, /*forward=*/false); |
| EXPECT_EQ(blink::mojom::FocusType::kNone, details.focus_type); |
| } |
| |
| // Regression test for https://crbug.com/326275041 |
| IN_PROC_BROWSER_TEST_P(PDFExtensionInteractiveUITest, SpaceKeyInForm) { |
| content::RenderFrameHost* extension_host = LoadPdfGetExtensionHost( |
| embedded_test_server()->GetURL("/pdf/text_form.pdf")); |
| ASSERT_TRUE(extension_host); |
| content::RenderFrameHost* plugin_host = |
| pdf_frame_util::FindPdfChildFrame(extension_host); |
| ASSERT_TRUE(plugin_host); |
| |
| static constexpr char kFocusChangeScript[] = R"( |
| var form_focus_changed = false; |
| const plugin = document.querySelector('embed'); |
| plugin.addEventListener('message', e => { |
| if (e.data.type === 'formFocusChange') { |
| form_focus_changed = true; |
| } |
| }); |
| )"; |
| ASSERT_TRUE(content::ExecJs(plugin_host, kFocusChangeScript)); |
| |
| // Since the PDF contains a text form that takes up the entire PDF page, this |
| // clicks into that form. |
| auto* embedder_web_contents = GetEmbedderWebContents(); |
| ASSERT_TRUE(embedder_web_contents); |
| SetInputFocusOnPlugin(extension_host, embedder_web_contents); |
| |
| // Wait for the input event to propagate into `plugin_host`. |
| while (true) { |
| // content::EvalJs() uses a base::RunLoop internally, so do not use |
| // base::test::RunUntil() here to avoid a double run loop. |
| if (content::EvalJs(plugin_host, "form_focus_changed").ExtractBool()) { |
| break; |
| } |
| } |
| |
| ASSERT_TRUE(ui_test_utils::SendKeyPressSync( |
| browser(), ui::VKEY_SPACE, /*control=*/false, /*shift=*/false, |
| /*alt=*/false, /*command=*/false)); |
| |
| content::RenderWidgetHostView* view = plugin_host->GetView(); |
| ASSERT_TRUE(view); |
| EXPECT_TRUE(view->GetSelectedText().empty()); |
| |
| // Wait for the key press to propagate from the browser to the PDF renderer. |
| // Keep calling SelectAll(), as it may arrive ahead of the key press event. |
| // Then wait for the text selection update to propagate from the PDF renderer |
| // back to the browser. |
| // |
| // If the space key press did not register at all, this hangs. |
| EXPECT_TRUE(base::test::RunUntil([embedder_web_contents, view]() { |
| embedder_web_contents->SelectAll(); |
| return view->GetSelectedText() == u" "; |
| })); |
| } |
| |
| #if defined(TOOLKIT_VIEWS) && defined(USE_AURA) |
| namespace { |
| |
| // Simulates a touch press event and touch release event on `contents` at |
| // `screen_pos`. Waits for the PDF viewer to notify `listener_host` that text |
| // has been selected in the PDF. |
| views::Widget* TouchSelectText(content::WebContents* contents, |
| content::RenderFrameHost* listener_host, |
| const gfx::Point& screen_pos) { |
| views::NamedWidgetShownWaiter waiter(views::test::AnyWidgetTestPasskey{}, |
| "TouchSelectionMenuViews"); |
| content::SimulateTouchEventAt(contents, ui::EventType::kTouchPressed, |
| screen_pos); |
| |
| EXPECT_EQ(true, content::EvalJs( |
| listener_host, |
| "new Promise(resolve => {" |
| " window.addEventListener('message', function(event) {" |
| " if (event.data.type == 'touchSelectionOccurred')" |
| " resolve(true);" |
| " });" |
| "});")); |
| |
| content::SimulateTouchEventAt(contents, ui::EventType::kTouchReleased, |
| screen_pos); |
| return waiter.WaitIfNeededAndGet(); |
| } |
| |
| } // namespace |
| |
| // On text selection, a touch selection menu should pop up. On clicking ellipsis |
| // icon on the menu, the context menu should open up. |
| IN_PROC_BROWSER_TEST_P(PDFExtensionInteractiveUITest, |
| ContextMenuOpensFromTouchSelectionMenu) { |
| const GURL url = embedded_test_server()->GetURL("/pdf/text_large.pdf"); |
| content::RenderFrameHost* extension_host = |
| LoadPdfInNewTabGetExtensionHost(url); |
| ASSERT_TRUE(extension_host); |
| |
| content::WaitForHitTestData(extension_host); |
| |
| content::WebContents* contents = GetActiveWebContents(); |
| |
| // For GuestView PDF viewer, the listener host can be the PDF embedder host. |
| // For OOPIF PDF viewer, the listener host can't be the embedder host, since |
| // the PDF extension host doesn't send it messages. Instead, the listener host |
| // can be the extension host and listen for messages from the PDF content |
| // host. |
| content::RenderFrameHost* listener_host = |
| UseOopif() ? extension_host : contents->GetPrimaryMainFrame(); |
| const gfx::Point point_in_root_coords = |
| extension_host->GetView()->TransformPointToRootCoordSpace( |
| ConvertPageCoordToScreenCoord(extension_host, {12, 12})); |
| views::Widget* widget = |
| TouchSelectText(contents, listener_host, point_in_root_coords); |
| ASSERT_TRUE(widget); |
| views::View* menu = widget->GetContentsView(); |
| ASSERT_TRUE(menu); |
| views::View* ellipsis_button = menu->GetViewByID( |
| views::TouchSelectionMenuViews::ButtonViewId::kEllipsisButton); |
| ASSERT_TRUE(ellipsis_button); |
| ContextMenuWaiter context_menu_observer; |
| ui::GestureEvent tap(0, 0, 0, ui::EventTimeForNow(), |
| ui::GestureEventDetails(ui::EventType::kGestureTap)); |
| ellipsis_button->OnGestureEvent(&tap); |
| context_menu_observer.WaitForMenuOpenAndClose(); |
| |
| // Verify that the expected context menu items are present. |
| // |
| // Note that the assertion below doesn't use exact matching via |
| // testing::ElementsAre, because some platforms may include unexpected extra |
| // elements (e.g. an extra separator and IDC=100 has been observed on some Mac |
| // bots). |
| EXPECT_THAT( |
| context_menu_observer.GetCapturedCommandIds(), |
| testing::IsSupersetOf( |
| {IDC_CONTENT_CONTEXT_COPY, IDC_CONTENT_CONTEXT_SEARCHWEBFOR, |
| IDC_PRINT, IDC_CONTENT_CONTEXT_ROTATECW, |
| IDC_CONTENT_CONTEXT_ROTATECCW, IDC_CONTENT_CONTEXT_INSPECTELEMENT})); |
| } |
| |
| // TODO(crbug.com/40847318): Deflake this test. |
| #if BUILDFLAG(IS_WIN) |
| #define MAYBE_TouchSelectionBounds DISABLED_TouchSelectionBounds |
| #else |
| #define MAYBE_TouchSelectionBounds TouchSelectionBounds |
| #endif // BUILDFLAG(IS_WIN) |
| IN_PROC_BROWSER_TEST_P(PDFExtensionInteractiveUITest, |
| MAYBE_TouchSelectionBounds) { |
| // Use test.pdf here because it has embedded font metrics. With a fixed zoom, |
| // coordinates should be consistent across platforms. |
| const GURL url = embedded_test_server()->GetURL("/pdf/test.pdf#zoom=100"); |
| content::RenderFrameHost* extension_host = |
| LoadPdfInNewTabGetExtensionHost(url); |
| ASSERT_TRUE(extension_host); |
| content::RenderFrameHost* plugin_host = |
| pdf_frame_util::FindPdfChildFrame(extension_host); |
| ASSERT_TRUE(plugin_host); |
| content::RenderWidgetHostView* view = plugin_host->GetView(); |
| ASSERT_TRUE(view); |
| EXPECT_TRUE(view->GetSelectedText().empty()); |
| |
| content::WaitForHitTestData(extension_host); |
| |
| content::WebContents* contents = GetActiveWebContents(); |
| |
| // For GuestView PDF viewer, the listener host can be the PDF embedder host. |
| // For OOPIF PDF viewer, the listener host can't be the embedder host, since |
| // the PDF extension host doesn't send it messages. Instead, the listener host |
| // can be the extension host and listen for messages from the PDF content |
| // host. |
| content::RenderFrameHost* listener_host = |
| UseOopif() ? extension_host : contents->GetPrimaryMainFrame(); |
| views::Widget* widget = TouchSelectText(contents, listener_host, {473, 166}); |
| ASSERT_TRUE(widget); |
| |
| EXPECT_EQ(u"some", view->GetSelectedText()); |
| |
| auto* touch_selection_controller = |
| extension_host->GetView() |
| ->GetTouchSelectionControllerClientManager() |
| ->GetTouchSelectionController(); |
| |
| gfx::SelectionBound start_bound = touch_selection_controller->start(); |
| EXPECT_EQ(gfx::SelectionBound::LEFT, start_bound.type()); |
| EXPECT_POINTF_NEAR(gfx::PointF(454.0f, 152.0f), start_bound.edge_start(), |
| 1.0f); |
| EXPECT_POINTF_NEAR(gfx::PointF(454.0f, 178.0f), start_bound.edge_end(), 1.0f); |
| |
| gfx::SelectionBound end_bound = touch_selection_controller->end(); |
| EXPECT_EQ(gfx::SelectionBound::RIGHT, end_bound.type()); |
| EXPECT_POINTF_NEAR(gfx::PointF(494.0f, 152.0f), end_bound.edge_start(), 1.0f); |
| EXPECT_POINTF_NEAR(gfx::PointF(494.0f, 178.0f), end_bound.edge_end(), 1.0f); |
| } |
| #endif // defined(TOOLKIT_VIEWS) && defined(USE_AURA) |
| |
| // TODO(crbug.com/40268279): Stop testing both modes after OOPIF PDF viewer |
| // launches. |
| INSTANTIATE_FEATURE_OVERRIDE_TEST_SUITE(PDFExtensionInteractiveUITest); |