| // Copyright (c) 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 "third_party/blink/renderer/core/html/forms/external_popup_menu.h" |
| |
| #include <memory> |
| |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/public/common/browser_interface_broker_proxy.h" |
| #include "third_party/blink/public/mojom/choosers/popup_menu.mojom-blink.h" |
| #include "third_party/blink/public/platform/web_url_loader_mock_factory.h" |
| #include "third_party/blink/public/web/web_popup_menu_info.h" |
| #include "third_party/blink/public/web/web_settings.h" |
| #include "third_party/blink/renderer/core/frame/frame_test_helpers.h" |
| #include "third_party/blink/renderer/core/frame/visual_viewport.h" |
| #include "third_party/blink/renderer/core/frame/web_local_frame_impl.h" |
| #include "third_party/blink/renderer/core/html/forms/html_select_element.h" |
| #include "third_party/blink/renderer/core/html/forms/popup_menu.h" |
| #include "third_party/blink/renderer/core/html/html_iframe_element.h" |
| #include "third_party/blink/renderer/core/html_names.h" |
| #include "third_party/blink/renderer/core/layout/layout_object.h" |
| #include "third_party/blink/renderer/core/page/page.h" |
| #include "third_party/blink/renderer/core/testing/fake_local_frame_host.h" |
| #include "third_party/blink/renderer/core/testing/page_test_base.h" |
| #include "third_party/blink/renderer/platform/heap/heap.h" |
| #include "third_party/blink/renderer/platform/testing/unit_test_helpers.h" |
| #include "third_party/blink/renderer/platform/testing/url_test_helpers.h" |
| |
| namespace blink { |
| |
| class ExternalPopupMenuDisplayNoneItemsTest : public PageTestBase { |
| public: |
| ExternalPopupMenuDisplayNoneItemsTest() = default; |
| |
| protected: |
| void SetUp() override { |
| PageTestBase::SetUp(); |
| auto* element = MakeGarbageCollected<HTMLSelectElement>(GetDocument()); |
| // Set the 4th an 5th items to have "display: none" property |
| element->setInnerHTML( |
| "<option><option><option><option style='display:none;'><option " |
| "style='display:none;'><option><option>"); |
| GetDocument().body()->AppendChild(element, ASSERT_NO_EXCEPTION); |
| owner_element_ = element; |
| GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kTest); |
| } |
| |
| Persistent<HTMLSelectElement> owner_element_; |
| }; |
| |
| TEST_F(ExternalPopupMenuDisplayNoneItemsTest, PopupMenuInfoSizeTest) { |
| int32_t item_height; |
| double font_size; |
| int32_t selected_item; |
| Vector<mojom::blink::MenuItemPtr> menu_items; |
| bool right_aligned; |
| bool allow_multiple_selection; |
| ExternalPopupMenu::GetPopupMenuInfo( |
| *owner_element_, &item_height, &font_size, &selected_item, &menu_items, |
| &right_aligned, &allow_multiple_selection); |
| EXPECT_EQ(5U, menu_items.size()); |
| } |
| |
| TEST_F(ExternalPopupMenuDisplayNoneItemsTest, IndexMappingTest) { |
| // 6th indexed item in popupmenu would be the 4th item in ExternalPopupMenu, |
| // and vice-versa. |
| EXPECT_EQ( |
| 4, ExternalPopupMenu::ToExternalPopupMenuItemIndex(6, *owner_element_)); |
| EXPECT_EQ(6, ExternalPopupMenu::ToPopupMenuItemIndex(4, *owner_element_)); |
| |
| // Invalid index, methods should return -1. |
| EXPECT_EQ( |
| -1, ExternalPopupMenu::ToExternalPopupMenuItemIndex(8, *owner_element_)); |
| EXPECT_EQ(-1, ExternalPopupMenu::ToPopupMenuItemIndex(8, *owner_element_)); |
| } |
| |
| class TestLocalFrameExternalPopupClient : public FakeLocalFrameHost { |
| public: |
| void ShowPopupMenu( |
| mojo::PendingRemote<mojom::blink::PopupMenuClient> popup_client, |
| const gfx::Rect& bounds, |
| int32_t item_height, |
| double font_size, |
| int32_t selected_item, |
| Vector<mojom::blink::MenuItemPtr> menu_items, |
| bool right_aligned, |
| bool allow_multiple_selection) override { |
| Reset(); |
| |
| bounds_ = bounds; |
| selected_item_ = selected_item; |
| menu_items_ = std::move(menu_items); |
| popup_client_.Bind(std::move(popup_client)); |
| popup_client_.set_disconnect_handler(base::BindOnce( |
| &TestLocalFrameExternalPopupClient::Reset, base::Unretained(this))); |
| std::move(showed_callback_).Run(); |
| } |
| |
| void Reset() { popup_client_.reset(); } |
| |
| void WaitUntilShowedPopup() { |
| base::RunLoop run_loop; |
| showed_callback_ = run_loop.QuitClosure(); |
| run_loop.Run(); |
| } |
| |
| mojom::blink::PopupMenuClient* PopupClient() { |
| DCHECK(popup_client_); |
| return popup_client_.get(); |
| } |
| |
| bool IsBound() const { return popup_client_.is_bound(); } |
| |
| const Vector<mojom::blink::MenuItemPtr>& MenuItems() const { |
| return menu_items_; |
| } |
| |
| int32_t SelectedItem() const { return selected_item_; } |
| |
| const gfx::Rect& ShownBounds() const { return bounds_; } |
| |
| private: |
| base::OnceClosure showed_callback_; |
| mojo::Remote<mojom::blink::PopupMenuClient> popup_client_; |
| int32_t selected_item_; |
| Vector<mojom::blink::MenuItemPtr> menu_items_; |
| gfx::Rect bounds_; |
| }; |
| |
| class ExternalPopupMenuTest : public testing::Test { |
| public: |
| ExternalPopupMenuTest() : base_url_("http://www.test.com") {} |
| |
| protected: |
| void SetUp() override { |
| frame_host_.Init( |
| web_frame_client_.GetRemoteNavigationAssociatedInterfaces()); |
| helper_.Initialize(&web_frame_client_); |
| WebView()->SetUseExternalPopupMenus(true); |
| } |
| void TearDown() override { |
| url_test_helpers::UnregisterAllURLsAndClearMemoryCache(); |
| } |
| |
| void RegisterMockedURLLoad(const std::string& file_name) { |
| // TODO(crbug.com/751425): We should use the mock functionality |
| // via |helper_|. |
| url_test_helpers::RegisterMockedURLLoadFromBase( |
| WebString::FromUTF8(base_url_), test::CoreTestDataPath("popup"), |
| WebString::FromUTF8(file_name), WebString::FromUTF8("text/html")); |
| } |
| |
| void LoadFrame(const std::string& file_name) { |
| frame_test_helpers::LoadFrame(MainFrame(), base_url_ + file_name); |
| WebView()->MainFrameViewWidget()->Resize(gfx::Size(800, 600)); |
| WebView()->MainFrameWidget()->UpdateAllLifecyclePhases( |
| DocumentUpdateReason::kTest); |
| } |
| |
| WebViewImpl* WebView() const { return helper_.GetWebView(); } |
| |
| const Vector<mojom::blink::MenuItemPtr>& MenuItems() const { |
| return frame_host_.MenuItems(); |
| } |
| |
| bool IsBound() const { return frame_host_.IsBound(); } |
| |
| int32_t SelectedItem() const { return frame_host_.SelectedItem(); } |
| |
| const gfx::Rect& ShownBounds() const { return frame_host_.ShownBounds(); } |
| |
| mojom::blink::PopupMenuClient* PopupClient() { |
| return frame_host_.PopupClient(); |
| } |
| |
| void WaitUntilShowedPopup() { frame_host_.WaitUntilShowedPopup(); } |
| |
| WebLocalFrameImpl* MainFrame() const { return helper_.LocalMainFrame(); } |
| |
| private: |
| TestLocalFrameExternalPopupClient frame_host_; |
| frame_test_helpers::TestWebFrameClient web_frame_client_; |
| std::string base_url_; |
| frame_test_helpers::WebViewHelper helper_; |
| }; |
| |
| TEST_F(ExternalPopupMenuTest, PopupAccountsForVisualViewportTransform) { |
| RegisterMockedURLLoad("select_mid_screen.html"); |
| LoadFrame("select_mid_screen.html"); |
| |
| WebView()->MainFrameViewWidget()->Resize(gfx::Size(100, 100)); |
| WebView()->MainFrameWidget()->UpdateAllLifecyclePhases( |
| DocumentUpdateReason::kTest); |
| |
| auto* select = To<HTMLSelectElement>( |
| MainFrame()->GetFrame()->GetDocument()->getElementById("select")); |
| auto* layout_object = select->GetLayoutObject(); |
| ASSERT_TRUE(layout_object); |
| |
| VisualViewport& visual_viewport = WebView()->GetPage()->GetVisualViewport(); |
| |
| IntRect rect_in_document = layout_object->AbsoluteBoundingBoxRect(); |
| |
| constexpr int kScaleFactor = 2; |
| ScrollOffset scroll_delta(20, 30); |
| |
| const int expected_x = |
| (rect_in_document.X() - scroll_delta.Width()) * kScaleFactor; |
| const int expected_y = |
| (rect_in_document.Y() - scroll_delta.Height()) * kScaleFactor; |
| |
| WebView()->SetPageScaleFactor(kScaleFactor); |
| visual_viewport.Move(scroll_delta); |
| select->ShowPopup(); |
| WaitUntilShowedPopup(); |
| |
| EXPECT_EQ(expected_x, ShownBounds().x()); |
| EXPECT_EQ(expected_y, ShownBounds().y()); |
| } |
| |
| TEST_F(ExternalPopupMenuTest, DidAcceptIndex) { |
| RegisterMockedURLLoad("select.html"); |
| LoadFrame("select.html"); |
| |
| auto* select = To<HTMLSelectElement>( |
| MainFrame()->GetFrame()->GetDocument()->getElementById("select")); |
| auto* layout_object = select->GetLayoutObject(); |
| ASSERT_TRUE(layout_object); |
| |
| select->ShowPopup(); |
| WaitUntilShowedPopup(); |
| |
| ASSERT_TRUE(select->PopupIsVisible()); |
| |
| PopupClient()->DidAcceptIndices({2}); |
| base::RunLoop().RunUntilIdle(); |
| |
| EXPECT_FALSE(select->PopupIsVisible()); |
| ASSERT_EQ("2", select->InnerElement().innerText().Utf8()); |
| EXPECT_EQ(2, select->selectedIndex()); |
| } |
| |
| TEST_F(ExternalPopupMenuTest, DidAcceptIndices) { |
| RegisterMockedURLLoad("select.html"); |
| LoadFrame("select.html"); |
| |
| auto* select = To<HTMLSelectElement>( |
| MainFrame()->GetFrame()->GetDocument()->getElementById("select")); |
| auto* layout_object = select->GetLayoutObject(); |
| ASSERT_TRUE(layout_object); |
| |
| select->ShowPopup(); |
| WaitUntilShowedPopup(); |
| |
| ASSERT_TRUE(select->PopupIsVisible()); |
| |
| PopupClient()->DidAcceptIndices({2}); |
| base::RunLoop().RunUntilIdle(); |
| |
| EXPECT_FALSE(select->PopupIsVisible()); |
| EXPECT_EQ("2", select->InnerElement().innerText()); |
| EXPECT_EQ(2, select->selectedIndex()); |
| } |
| |
| TEST_F(ExternalPopupMenuTest, DidAcceptIndicesClearSelect) { |
| RegisterMockedURLLoad("select.html"); |
| LoadFrame("select.html"); |
| |
| auto* select = To<HTMLSelectElement>( |
| MainFrame()->GetFrame()->GetDocument()->getElementById("select")); |
| auto* layout_object = select->GetLayoutObject(); |
| ASSERT_TRUE(layout_object); |
| |
| select->ShowPopup(); |
| WaitUntilShowedPopup(); |
| |
| ASSERT_TRUE(select->PopupIsVisible()); |
| PopupClient()->DidAcceptIndices({}); |
| base::RunLoop().RunUntilIdle(); |
| |
| EXPECT_FALSE(select->PopupIsVisible()); |
| EXPECT_EQ(-1, select->selectedIndex()); |
| } |
| |
| // Normal case: test showing a select popup, canceling/selecting an item. |
| TEST_F(ExternalPopupMenuTest, NormalCase) { |
| RegisterMockedURLLoad("select.html"); |
| LoadFrame("select.html"); |
| |
| // Show the popup-menu. |
| auto* select = To<HTMLSelectElement>( |
| MainFrame()->GetFrame()->GetDocument()->getElementById("select")); |
| auto* layout_object = select->GetLayoutObject(); |
| ASSERT_TRUE(layout_object); |
| |
| select->ShowPopup(); |
| WaitUntilShowedPopup(); |
| |
| ASSERT_TRUE(select->PopupIsVisible()); |
| ASSERT_EQ(3U, MenuItems().size()); |
| EXPECT_EQ(1, SelectedItem()); |
| |
| // Simulate the user canceling the popup; the index should not have changed. |
| PopupClient()->DidCancel(); |
| base::RunLoop().RunUntilIdle(); |
| EXPECT_EQ(1, select->selectedIndex()); |
| |
| // Show the pop-up again and this time make a selection. |
| select->ShowPopup(); |
| WaitUntilShowedPopup(); |
| |
| PopupClient()->DidAcceptIndices({0}); |
| base::RunLoop().RunUntilIdle(); |
| EXPECT_EQ(0, select->selectedIndex()); |
| |
| // Show the pop-up again and make another selection. |
| select->ShowPopup(); |
| WaitUntilShowedPopup(); |
| |
| ASSERT_EQ(3U, MenuItems().size()); |
| EXPECT_EQ(0, SelectedItem()); |
| } |
| |
| // Page shows popup, then navigates away while popup showing, then select. |
| TEST_F(ExternalPopupMenuTest, ShowPopupThenNavigate) { |
| RegisterMockedURLLoad("select.html"); |
| LoadFrame("select.html"); |
| |
| // Show the popup-menu. |
| auto* document = MainFrame()->GetFrame()->GetDocument(); |
| auto* select = To<HTMLSelectElement>(document->getElementById("select")); |
| auto* layout_object = select->GetLayoutObject(); |
| ASSERT_TRUE(layout_object); |
| |
| select->ShowPopup(); |
| WaitUntilShowedPopup(); |
| |
| // Now we navigate to another pager. |
| document->documentElement()->setInnerHTML("<blink>Awesome page!</blink>"); |
| document->UpdateStyleAndLayout(DocumentUpdateReason::kTest); |
| base::RunLoop().RunUntilIdle(); |
| |
| // Now HTMLSelectElement should be nullptr and mojo is disconnected. |
| select = To<HTMLSelectElement>(document->getElementById("select")); |
| EXPECT_FALSE(select); |
| EXPECT_FALSE(IsBound()); |
| } |
| |
| // An empty select should not cause a crash when clicked. |
| // http://crbug.com/63774 |
| TEST_F(ExternalPopupMenuTest, EmptySelect) { |
| RegisterMockedURLLoad("select.html"); |
| LoadFrame("select.html"); |
| |
| auto* select = To<HTMLSelectElement>( |
| MainFrame()->GetFrame()->GetDocument()->getElementById("emptySelect")); |
| EXPECT_TRUE(select); |
| select->click(); |
| } |
| |
| // Tests that nothing bad happen when the page removes the select when it |
| // changes. (http://crbug.com/61997) |
| TEST_F(ExternalPopupMenuTest, RemoveOnChange) { |
| RegisterMockedURLLoad("select_event_remove_on_change.html"); |
| LoadFrame("select_event_remove_on_change.html"); |
| |
| // Show the popup-menu. |
| auto* document = MainFrame()->GetFrame()->GetDocument(); |
| auto* select = To<HTMLSelectElement>(document->getElementById("s")); |
| auto* layout_object = select->GetLayoutObject(); |
| ASSERT_TRUE(layout_object); |
| |
| select->ShowPopup(); |
| WaitUntilShowedPopup(); |
| |
| // Select something, it causes the select to be removed from the page. |
| PopupClient()->DidAcceptIndices({1}); |
| base::RunLoop().RunUntilIdle(); |
| |
| // Just to check the soundness of the test. |
| // It should return nullptr as the select has been removed. |
| select = To<HTMLSelectElement>(document->getElementById("s")); |
| EXPECT_FALSE(select); |
| } |
| |
| // crbug.com/912211 |
| TEST_F(ExternalPopupMenuTest, RemoveFrameOnChange) { |
| RegisterMockedURLLoad("select_event_remove_frame_on_change.html"); |
| LoadFrame("select_event_remove_frame_on_change.html"); |
| |
| // Open a popup. |
| auto* iframe = To<HTMLIFrameElement>( |
| MainFrame()->GetFrame()->GetDocument()->QuerySelector("iframe")); |
| auto* select = |
| To<HTMLSelectElement>(iframe->contentDocument()->QuerySelector("select")); |
| auto* layout_object = select->GetLayoutObject(); |
| ASSERT_TRUE(layout_object); |
| |
| select->ShowPopup(); |
| |
| // Select something on the sub-frame, it causes the frame to be removed from |
| // the page. |
| select->SelectOptionByPopup(1); |
| // The test passes if the test didn't crash and ASAN didn't complain. |
| } |
| |
| } // namespace blink |