1 /* 2 * Copyright (C) 2010 Google Inc. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions are 6 * met: 7 * 8 * * Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * * Redistributions in binary form must reproduce the above 11 * copyright notice, this list of conditions and the following disclaimer 12 * in the documentation and/or other materials provided with the 13 * distribution. 14 * * Neither the name of Google Inc. nor the names of its 15 * contributors may be used to endorse or promote products derived from 16 * this software without specific prior written permission. 17 * 18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 */ 30 31 #include "config.h" 32 33 #include "core/dom/Element.h" 34 #include "core/frame/FrameView.h" 35 #include "core/frame/Settings.h" 36 #include "core/html/HTMLSelectElement.h" 37 #include "core/page/EventHandler.h" 38 #include "platform/KeyboardCodes.h" 39 #include "platform/PlatformMouseEvent.h" 40 #include "platform/PopupMenu.h" 41 #include "platform/PopupMenuClient.h" 42 #include "platform/RuntimeEnabledFeatures.h" 43 #include "platform/graphics/Color.h" 44 #include "platform/scroll/ScrollbarTheme.h" 45 #include "public/platform/Platform.h" 46 #include "public/platform/WebScreenInfo.h" 47 #include "public/platform/WebString.h" 48 #include "public/platform/WebURL.h" 49 #include "public/platform/WebURLRequest.h" 50 #include "public/platform/WebURLResponse.h" 51 #include "public/platform/WebUnitTestSupport.h" 52 #include "public/web/WebDocument.h" 53 #include "public/web/WebElement.h" 54 #include "public/web/WebFrame.h" 55 #include "public/web/WebFrameClient.h" 56 #include "public/web/WebInputEvent.h" 57 #include "public/web/WebSettings.h" 58 #include "public/web/WebView.h" 59 #include "public/web/WebViewClient.h" 60 #include "v8.h" 61 #include "web/PopupContainer.h" 62 #include "web/PopupListBox.h" 63 #include "web/PopupMenuChromium.h" 64 #include "web/WebLocalFrameImpl.h" 65 #include "web/WebPopupMenuImpl.h" 66 #include "web/WebViewImpl.h" 67 #include "web/tests/FrameTestHelpers.h" 68 #include "web/tests/URLTestHelpers.h" 69 #include <gtest/gtest.h> 70 71 using namespace WebCore; 72 using namespace blink; 73 using blink::URLTestHelpers::toKURL; 74 75 namespace { 76 77 class TestPopupMenuClient : public PopupMenuClient { 78 public: 79 // Item at index 0 is selected by default. 80 TestPopupMenuClient() : m_selectIndex(0), m_node(0), m_listSize(10) { } 81 virtual ~TestPopupMenuClient() {} 82 virtual void valueChanged(unsigned listIndex, bool fireEvents = true) 83 { 84 m_selectIndex = listIndex; 85 if (m_node) { 86 HTMLSelectElement* select = toHTMLSelectElement(m_node); 87 select->optionSelectedByUser(select->listToOptionIndex(listIndex), fireEvents); 88 } 89 } 90 virtual void selectionChanged(unsigned, bool) {} 91 virtual void selectionCleared() {} 92 93 virtual String itemText(unsigned listIndex) const 94 { 95 String str("Item "); 96 str.append(String::number(listIndex)); 97 return str; 98 } 99 virtual String itemLabel(unsigned) const { return String(); } 100 virtual String itemIcon(unsigned) const { return String(); } 101 virtual String itemToolTip(unsigned listIndex) const { return itemText(listIndex); } 102 virtual String itemAccessibilityText(unsigned listIndex) const { return itemText(listIndex); } 103 virtual bool itemIsEnabled(unsigned listIndex) const { return m_disabledIndexSet.find(listIndex) == m_disabledIndexSet.end(); } 104 virtual PopupMenuStyle itemStyle(unsigned listIndex) const 105 { 106 FontDescription fontDescription; 107 fontDescription.setComputedSize(12.0); 108 Font font(fontDescription); 109 font.update(nullptr); 110 return PopupMenuStyle(Color::black, Color::white, font, true, false, Length(), TextDirection(), false /* has text direction override */); 111 } 112 virtual PopupMenuStyle menuStyle() const { return itemStyle(0); } 113 virtual int clientInsetLeft() const { return 0; } 114 virtual int clientInsetRight() const { return 0; } 115 virtual LayoutUnit clientPaddingLeft() const { return 0; } 116 virtual LayoutUnit clientPaddingRight() const { return 0; } 117 virtual int listSize() const { return m_listSize; } 118 virtual int selectedIndex() const { return m_selectIndex; } 119 virtual void popupDidHide() { } 120 virtual bool itemIsSeparator(unsigned listIndex) const { return false; } 121 virtual bool itemIsLabel(unsigned listIndex) const { return false; } 122 virtual bool itemIsSelected(unsigned listIndex) const { return listIndex == m_selectIndex; } 123 virtual bool valueShouldChangeOnHotTrack() const { return false; } 124 virtual void setTextFromItem(unsigned listIndex) { } 125 126 virtual FontSelector* fontSelector() const { return 0; } 127 virtual HostWindow* hostWindow() const { return 0; } 128 129 virtual PassRefPtr<Scrollbar> createScrollbar(ScrollableArea*, ScrollbarOrientation, ScrollbarControlSize) { return nullptr; } 130 131 void setDisabledIndex(unsigned index) { m_disabledIndexSet.insert(index); } 132 void setFocusedNode(Node* node) { m_node = node; } 133 void setListSize(int listSize) { m_listSize = listSize; } 134 135 private: 136 unsigned m_selectIndex; 137 std::set<unsigned> m_disabledIndexSet; 138 Node* m_node; 139 int m_listSize; 140 }; 141 142 class TestWebWidgetClient : public WebWidgetClient { 143 public: 144 ~TestWebWidgetClient() { } 145 }; 146 147 class TestWebPopupMenuImpl : public WebPopupMenuImpl { 148 public: 149 static PassRefPtr<TestWebPopupMenuImpl> create(WebWidgetClient* client) 150 { 151 return adoptRef(new TestWebPopupMenuImpl(client)); 152 } 153 154 ~TestWebPopupMenuImpl() { } 155 156 private: 157 TestWebPopupMenuImpl(WebWidgetClient* client) : WebPopupMenuImpl(client) { } 158 }; 159 160 class PopupTestWebViewClient : public FrameTestHelpers::TestWebViewClient { 161 public: 162 PopupTestWebViewClient() : m_webPopupMenu(TestWebPopupMenuImpl::create(&m_webWidgetClient)) { } 163 ~PopupTestWebViewClient() { } 164 165 virtual WebWidget* createPopupMenu(WebPopupType) { return m_webPopupMenu.get(); } 166 167 // We need to override this so that the popup menu size is not 0 168 // (the layout code checks to see if the popup fits on the screen). 169 virtual WebScreenInfo screenInfo() 170 { 171 WebScreenInfo screenInfo; 172 screenInfo.availableRect.height = 2000; 173 screenInfo.availableRect.width = 2000; 174 return screenInfo; 175 } 176 177 private: 178 TestWebWidgetClient m_webWidgetClient; 179 RefPtr<TestWebPopupMenuImpl> m_webPopupMenu; 180 }; 181 182 class SelectPopupMenuTest : public testing::Test { 183 public: 184 SelectPopupMenuTest() 185 : baseURL("http://www.test.com/") 186 { 187 } 188 189 protected: 190 virtual void SetUp() 191 { 192 m_helper.initialize(false, 0, &m_webviewClient); 193 m_popupMenu = adoptRef(new PopupMenuChromium(*mainFrame()->frame(), &m_popupMenuClient)); 194 } 195 196 virtual void TearDown() 197 { 198 m_popupMenu = nullptr; 199 Platform::current()->unitTestSupport()->unregisterAllMockedURLs(); 200 } 201 202 // Returns true if there currently is a select popup in the WebView. 203 bool popupOpen() const { return webView()->selectPopup(); } 204 205 int selectedIndex() const { return m_popupMenuClient.selectedIndex(); } 206 207 void showPopup() 208 { 209 m_popupMenu->show(FloatQuad(FloatRect(0, 0, 100, 100)), IntSize(100, 100), 0); 210 ASSERT_TRUE(popupOpen()); 211 } 212 213 void hidePopup() 214 { 215 m_popupMenu->hide(); 216 EXPECT_FALSE(popupOpen()); 217 } 218 219 void simulateKeyDownEvent(int keyCode) 220 { 221 simulateKeyEvent(WebInputEvent::RawKeyDown, keyCode); 222 } 223 224 void simulateKeyUpEvent(int keyCode) 225 { 226 simulateKeyEvent(WebInputEvent::KeyUp, keyCode); 227 } 228 229 // Simulates a key event on the WebView. 230 // The WebView forwards the event to the select popup if one is open. 231 void simulateKeyEvent(WebInputEvent::Type eventType, int keyCode) 232 { 233 WebKeyboardEvent keyEvent; 234 keyEvent.windowsKeyCode = keyCode; 235 keyEvent.type = eventType; 236 webView()->handleInputEvent(keyEvent); 237 } 238 239 // Simulates a mouse event on the select popup. 240 void simulateLeftMouseDownEvent(const IntPoint& point) 241 { 242 PlatformMouseEvent mouseEvent(point, point, LeftButton, PlatformEvent::MousePressed, 243 1, false, false, false, false, 0); 244 webView()->selectPopup()->handleMouseDownEvent(mouseEvent); 245 } 246 void simulateLeftMouseUpEvent(const IntPoint& point) 247 { 248 PlatformMouseEvent mouseEvent(point, point, LeftButton, PlatformEvent::MouseReleased, 249 1, false, false, false, false, 0); 250 webView()->selectPopup()->handleMouseReleaseEvent(mouseEvent); 251 } 252 253 void registerMockedURLLoad(const std::string& fileName) 254 { 255 URLTestHelpers::registerMockedURLLoad(toKURL(baseURL + fileName), WebString::fromUTF8(fileName.c_str()), WebString::fromUTF8("popup/"), WebString::fromUTF8("text/html")); 256 } 257 258 void loadFrame(WebFrame* frame, const std::string& fileName) 259 { 260 FrameTestHelpers::loadFrame(frame, baseURL + fileName); 261 } 262 263 WebViewImpl* webView() const { return m_helper.webViewImpl(); } 264 WebLocalFrameImpl* mainFrame() const { return m_helper.webViewImpl()->mainFrameImpl(); } 265 266 protected: 267 PopupTestWebViewClient m_webviewClient; 268 TestPopupMenuClient m_popupMenuClient; 269 RefPtr<PopupMenu> m_popupMenu; 270 std::string baseURL; 271 272 private: 273 FrameTestHelpers::WebViewHelper m_helper; 274 }; 275 276 // Tests that show/hide and repeats. Select popups are reused in web pages when 277 // they are reopened, that what this is testing. 278 TEST_F(SelectPopupMenuTest, ShowThenHide) 279 { 280 for (int i = 0; i < 3; i++) { 281 showPopup(); 282 hidePopup(); 283 } 284 } 285 286 // Tests that showing a select popup and deleting it does not cause problem. 287 // This happens in real-life if a page navigates while a select popup is showing. 288 TEST_F(SelectPopupMenuTest, ShowThenDelete) 289 { 290 showPopup(); 291 // Nothing else to do, TearDown() deletes the popup. 292 } 293 294 // Tests that losing focus closes the select popup. 295 TEST_F(SelectPopupMenuTest, ShowThenLoseFocus) 296 { 297 showPopup(); 298 // Simulate losing focus. 299 webView()->setFocus(false); 300 301 // Popup should have closed. 302 EXPECT_FALSE(popupOpen()); 303 } 304 305 // Tests that pressing ESC closes the popup. 306 TEST_F(SelectPopupMenuTest, ShowThenPressESC) 307 { 308 showPopup(); 309 simulateKeyDownEvent(VKEY_ESCAPE); 310 // Popup should have closed. 311 EXPECT_FALSE(popupOpen()); 312 } 313 314 // Tests selecting an item with the arrows and enter/esc/tab. 315 TEST_F(SelectPopupMenuTest, SelectWithKeys) 316 { 317 showPopup(); 318 // Simulate selecting the 2nd item by pressing Down, Down, enter. 319 simulateKeyDownEvent(VKEY_DOWN); 320 simulateKeyDownEvent(VKEY_DOWN); 321 simulateKeyDownEvent(VKEY_RETURN); 322 323 // Popup should have closed. 324 EXPECT_TRUE(!popupOpen()); 325 EXPECT_EQ(2, selectedIndex()); 326 327 // It should work as well with ESC. 328 showPopup(); 329 simulateKeyDownEvent(VKEY_DOWN); 330 simulateKeyDownEvent(VKEY_ESCAPE); 331 EXPECT_FALSE(popupOpen()); 332 EXPECT_EQ(3, selectedIndex()); 333 334 // It should work as well with TAB. 335 showPopup(); 336 simulateKeyDownEvent(VKEY_DOWN); 337 simulateKeyDownEvent(VKEY_TAB); 338 EXPECT_FALSE(popupOpen()); 339 EXPECT_EQ(4, selectedIndex()); 340 } 341 342 // Tests that selecting an item with the mouse does select the item and close 343 // the popup. 344 TEST_F(SelectPopupMenuTest, ClickItem) 345 { 346 showPopup(); 347 348 int menuItemHeight = webView()->selectPopup()->menuItemHeight(); 349 // menuItemHeight * 1.5 means the Y position on the item at index 1. 350 IntPoint row1Point(2, menuItemHeight * 1.5); 351 // Simulate a click down/up on the first item. 352 simulateLeftMouseDownEvent(row1Point); 353 simulateLeftMouseUpEvent(row1Point); 354 355 // Popup should have closed and the item at index 1 selected. 356 EXPECT_FALSE(popupOpen()); 357 EXPECT_EQ(1, selectedIndex()); 358 } 359 360 // Tests that moving the mouse over an item and then clicking outside the select popup 361 // leaves the seleted item unchanged. 362 TEST_F(SelectPopupMenuTest, MouseOverItemClickOutside) 363 { 364 showPopup(); 365 366 int menuItemHeight = webView()->selectPopup()->menuItemHeight(); 367 // menuItemHeight * 1.5 means the Y position on the item at index 1. 368 IntPoint row1Point(2, menuItemHeight * 1.5); 369 // Simulate the mouse moving over the first item. 370 PlatformMouseEvent mouseEvent(row1Point, row1Point, NoButton, PlatformEvent::MouseMoved, 371 1, false, false, false, false, 0); 372 webView()->selectPopup()->handleMouseMoveEvent(mouseEvent); 373 374 // Click outside the popup. 375 simulateLeftMouseDownEvent(IntPoint(1000, 1000)); 376 377 // Popup should have closed and item 0 should still be selected. 378 EXPECT_FALSE(popupOpen()); 379 EXPECT_EQ(0, selectedIndex()); 380 } 381 382 // Tests that selecting an item with the keyboard and then clicking outside the select 383 // popup does select that item. 384 TEST_F(SelectPopupMenuTest, SelectItemWithKeyboardItemClickOutside) 385 { 386 showPopup(); 387 388 // Simulate selecting the 2nd item by pressing Down, Down. 389 simulateKeyDownEvent(VKEY_DOWN); 390 simulateKeyDownEvent(VKEY_DOWN); 391 392 // Click outside the popup. 393 simulateLeftMouseDownEvent(IntPoint(1000, 1000)); 394 395 // Popup should have closed and the item should have been selected. 396 EXPECT_FALSE(popupOpen()); 397 EXPECT_EQ(2, selectedIndex()); 398 } 399 400 TEST_F(SelectPopupMenuTest, DISABLED_SelectItemEventFire) 401 { 402 registerMockedURLLoad("select_event.html"); 403 webView()->settings()->setJavaScriptEnabled(true); 404 loadFrame(mainFrame(), "select_event.html"); 405 406 m_popupMenuClient.setFocusedNode(mainFrame()->frame()->document()->focusedElement()); 407 408 showPopup(); 409 410 int menuItemHeight = webView()->selectPopup()->menuItemHeight(); 411 // menuItemHeight * 0.5 means the Y position on the item at index 0. 412 IntPoint row1Point(2, menuItemHeight * 0.5); 413 simulateLeftMouseDownEvent(row1Point); 414 simulateLeftMouseUpEvent(row1Point); 415 416 WebElement element = webView()->mainFrame()->document().getElementById("message"); 417 418 // mousedown event is held by select node, and we don't simulate the event for the node. 419 // So we can only see mouseup and click event. 420 EXPECT_STREQ("upclick", element.innerText().utf8().data()); 421 422 // Disable the item at index 1. 423 m_popupMenuClient.setDisabledIndex(1); 424 425 showPopup(); 426 // menuItemHeight * 1.5 means the Y position on the item at index 1. 427 row1Point.setY(menuItemHeight * 1.5); 428 simulateLeftMouseDownEvent(row1Point); 429 simulateLeftMouseUpEvent(row1Point); 430 431 // The item at index 1 is disabled, so the text should not be changed. 432 EXPECT_STREQ("upclick", element.innerText().utf8().data()); 433 434 showPopup(); 435 // menuItemHeight * 2.5 means the Y position on the item at index 2. 436 row1Point.setY(menuItemHeight * 2.5); 437 simulateLeftMouseDownEvent(row1Point); 438 simulateLeftMouseUpEvent(row1Point); 439 440 // The item is changed to the item at index 2, from index 0, so change event is fired. 441 EXPECT_STREQ("upclickchangeupclick", element.innerText().utf8().data()); 442 } 443 444 TEST_F(SelectPopupMenuTest, FLAKY_SelectItemKeyEvent) 445 { 446 registerMockedURLLoad("select_event.html"); 447 webView()->settings()->setJavaScriptEnabled(true); 448 loadFrame(mainFrame(), "select_event.html"); 449 450 m_popupMenuClient.setFocusedNode(mainFrame()->frame()->document()->focusedElement()); 451 452 showPopup(); 453 454 // Siumulate to choose the item at index 1 with keyboard. 455 simulateKeyDownEvent(VKEY_DOWN); 456 simulateKeyDownEvent(VKEY_DOWN); 457 simulateKeyDownEvent(VKEY_RETURN); 458 459 WebElement element = webView()->mainFrame()->document().getElementById("message"); 460 // We only can see change event but no other mouse related events. 461 EXPECT_STREQ("change", element.innerText().utf8().data()); 462 } 463 464 TEST_F(SelectPopupMenuTest, SelectItemRemoveSelectOnChange) 465 { 466 // Make sure no crash, even if select node is removed on 'change' event handler. 467 registerMockedURLLoad("select_event_remove_on_change.html"); 468 webView()->settings()->setJavaScriptEnabled(true); 469 loadFrame(mainFrame(), "select_event_remove_on_change.html"); 470 471 m_popupMenuClient.setFocusedNode(mainFrame()->frame()->document()->focusedElement()); 472 473 showPopup(); 474 475 int menuItemHeight = webView()->selectPopup()->menuItemHeight(); 476 // menuItemHeight * 1.5 means the Y position on the item at index 1. 477 IntPoint row1Point(2, menuItemHeight * 1.5); 478 simulateLeftMouseDownEvent(row1Point); 479 simulateLeftMouseUpEvent(row1Point); 480 481 WebElement element = webView()->mainFrame()->document().getElementById("message"); 482 EXPECT_STREQ("change", element.innerText().utf8().data()); 483 } 484 485 TEST_F(SelectPopupMenuTest, SelectItemRemoveSelectOnClick) 486 { 487 // Make sure no crash, even if select node is removed on 'click' event handler. 488 registerMockedURLLoad("select_event_remove_on_click.html"); 489 webView()->settings()->setJavaScriptEnabled(true); 490 loadFrame(mainFrame(), "select_event_remove_on_click.html"); 491 492 m_popupMenuClient.setFocusedNode(mainFrame()->frame()->document()->focusedElement()); 493 494 showPopup(); 495 496 int menuItemHeight = webView()->selectPopup()->menuItemHeight(); 497 // menuItemHeight * 1.5 means the Y position on the item at index 1. 498 IntPoint row1Point(2, menuItemHeight * 1.5); 499 simulateLeftMouseDownEvent(row1Point); 500 simulateLeftMouseUpEvent(row1Point); 501 502 WebElement element = webView()->mainFrame()->document().getElementById("message"); 503 EXPECT_STREQ("click", element.innerText().utf8().data()); 504 } 505 506 #if OS(ANDROID) 507 TEST_F(SelectPopupMenuTest, DISABLED_PopupListBoxWithOverlayScrollbarEnabled) 508 #else 509 TEST_F(SelectPopupMenuTest, PopupListBoxWithOverlayScrollbarEnabled) 510 #endif 511 { 512 Settings::setMockScrollbarsEnabled(true); 513 RuntimeEnabledFeatures::setOverlayScrollbarsEnabled(true); 514 EXPECT_TRUE(ScrollbarTheme::theme()->usesOverlayScrollbars()); 515 registerMockedURLLoad("select_rtl.html"); 516 loadFrame(mainFrame(), "select_rtl.html"); 517 518 m_popupMenuClient.setFocusedNode(mainFrame()->frame()->document()->focusedElement()); 519 m_popupMenuClient.setListSize(30); 520 521 showPopup(); 522 PopupContainer* container = webView()->selectPopup(); 523 PopupListBox* listBox = container->listBox(); 524 525 EXPECT_EQ(container->width(), listBox->contentsSize().width() + 2); 526 Settings::setMockScrollbarsEnabled(false); 527 RuntimeEnabledFeatures::setOverlayScrollbarsEnabled(false); 528 EXPECT_FALSE(ScrollbarTheme::theme()->usesOverlayScrollbars()); 529 } 530 531 #if OS(ANDROID) 532 TEST_F(SelectPopupMenuTest, DISABLED_PopupListBoxWithOverlayScrollbarDisabled) 533 #else 534 TEST_F(SelectPopupMenuTest, PopupListBoxWithOverlayScrollbarDisabled) 535 #endif 536 { 537 EXPECT_FALSE(ScrollbarTheme::theme()->usesOverlayScrollbars()); 538 registerMockedURLLoad("select_rtl.html"); 539 loadFrame(mainFrame(), "select_rtl.html"); 540 541 m_popupMenuClient.setFocusedNode(mainFrame()->frame()->document()->focusedElement()); 542 m_popupMenuClient.setListSize(30); 543 544 showPopup(); 545 PopupContainer* container = webView()->selectPopup(); 546 PopupListBox* listBox = container->listBox(); 547 548 EXPECT_EQ(container->width(), listBox->contentsSize().width() + ScrollbarTheme::theme()->scrollbarThickness() + 2); 549 } 550 551 } // namespace 552