| /* |
| * Copyright (C) 2013 Google Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions are |
| * met: |
| * |
| * * Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * * Redistributions in binary form must reproduce the above |
| * copyright notice, this list of conditions and the following disclaimer |
| * in the documentation and/or other materials provided with the |
| * distribution. |
| * * Neither the name of Google Inc. nor the names of its |
| * contributors may be used to endorse or promote products derived from |
| * this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/public/platform/platform.h" |
| #include "third_party/blink/public/platform/web_coalesced_input_event.h" |
| #include "third_party/blink/public/platform/web_touch_event.h" |
| #include "third_party/blink/public/platform/web_url_loader_mock_factory.h" |
| #include "third_party/blink/public/web/web_document.h" |
| #include "third_party/blink/public/web/web_frame.h" |
| #include "third_party/blink/public/web/web_hit_test_result.h" |
| #include "third_party/blink/public/web/web_view.h" |
| #include "third_party/blink/public/web/web_view_client.h" |
| #include "third_party/blink/public/web/web_widget_client.h" |
| #include "third_party/blink/renderer/core/dom/document.h" |
| #include "third_party/blink/renderer/core/dom/element.h" |
| #include "third_party/blink/renderer/core/dom/shadow_root.h" |
| #include "third_party/blink/renderer/core/dom/static_node_list.h" |
| #include "third_party/blink/renderer/core/exported/web_view_impl.h" |
| #include "third_party/blink/renderer/core/frame/frame_test_helpers.h" |
| #include "third_party/blink/renderer/core/frame/local_frame.h" |
| #include "third_party/blink/renderer/core/frame/local_frame_view.h" |
| #include "third_party/blink/renderer/core/frame/web_local_frame_impl.h" |
| #include "third_party/blink/renderer/core/geometry/dom_rect.h" |
| #include "third_party/blink/renderer/core/geometry/dom_rect_list.h" |
| #include "third_party/blink/renderer/core/html/html_iframe_element.h" |
| #include "third_party/blink/renderer/core/input/event_handler.h" |
| #include "third_party/blink/renderer/core/layout/hit_test_result.h" |
| #include "third_party/blink/renderer/core/layout/layout_tree_as_text.h" |
| #include "third_party/blink/renderer/core/layout/layout_view.h" |
| #include "third_party/blink/renderer/core/paint/paint_layer_scrollable_area.h" |
| #include "third_party/blink/renderer/platform/testing/unit_test_helpers.h" |
| #include "third_party/blink/renderer/platform/testing/url_test_helpers.h" |
| |
| using blink::test::RunPendingTasks; |
| |
| namespace blink { |
| |
| class TouchActionTrackingWebWidgetClient |
| : public FrameTestHelpers::TestWebWidgetClient { |
| public: |
| TouchActionTrackingWebWidgetClient() |
| : action_set_count_(0), action_(TouchAction::kTouchActionAuto) {} |
| |
| // WebWidgetClient methods |
| void SetTouchAction(TouchAction touch_action) override { |
| action_set_count_++; |
| action_ = touch_action; |
| } |
| |
| // Local methods |
| void Reset() { |
| action_set_count_ = 0; |
| action_ = TouchAction::kTouchActionAuto; |
| } |
| |
| int TouchActionSetCount() { return action_set_count_; } |
| |
| TouchAction LastTouchAction() { return action_; } |
| |
| private: |
| int action_set_count_; |
| TouchAction action_; |
| }; |
| |
| class TouchActionTest : public testing::Test { |
| public: |
| TouchActionTest() : base_url_("http://www.test.com/") { |
| URLTestHelpers::RegisterMockedURLLoadFromBase( |
| WebString::FromUTF8(base_url_), test::CoreTestDataPath(), |
| "touch-action-tests.css", "text/css"); |
| URLTestHelpers::RegisterMockedURLLoadFromBase( |
| WebString::FromUTF8(base_url_), test::CoreTestDataPath(), |
| "touch-action-tests.js", "text/javascript"); |
| URLTestHelpers::RegisterMockedURLLoadFromBase( |
| WebString::FromUTF8(base_url_), test::CoreTestDataPath(), |
| "white-1x1.png", "image/png"); |
| } |
| |
| void TearDown() override { |
| Platform::Current() |
| ->GetURLLoaderMockFactory() |
| ->UnregisterAllURLsAndClearMemoryCache(); |
| } |
| |
| protected: |
| void RunTouchActionTest(std::string file); |
| void RunShadowDOMTest(std::string file); |
| void RunIFrameTest(std::string file); |
| void SendTouchEvent(WebView*, WebInputEvent::Type, IntPoint client_point); |
| WebViewImpl* SetupTest(std::string file, TouchActionTrackingWebWidgetClient&); |
| void RunTestOnTree(ContainerNode* root, |
| WebView*, |
| TouchActionTrackingWebWidgetClient&); |
| |
| std::string base_url_; |
| FrameTestHelpers::WebViewHelper web_view_helper_; |
| }; |
| |
| void TouchActionTest::RunTouchActionTest(std::string file) { |
| TouchActionTrackingWebWidgetClient client; |
| |
| // runTouchActionTest() loads a document in a frame, setting up a |
| // nested run loop. Should any Oilpan GC happen while it is in |
| // effect, the implicit assumption that we're outside any event |
| // loop (=> there being no pointers on the stack needing scanning) |
| // when that GC strikes will no longer hold. |
| // |
| // To ensure that the references on the stack are also traced, we |
| // turn them into persistent, stack allocated references. This |
| // workaround is sufficient to handle this artificial test |
| // scenario. |
| WebViewImpl* web_view = SetupTest(file, client); |
| |
| Persistent<Document> document = |
| static_cast<Document*>(web_view->MainFrameImpl()->GetDocument()); |
| RunTestOnTree(document.Get(), web_view, client); |
| |
| // Explicitly reset to break dependency on locally scoped client. |
| web_view_helper_.Reset(); |
| } |
| |
| void TouchActionTest::RunShadowDOMTest(std::string file) { |
| TouchActionTrackingWebWidgetClient client; |
| |
| WebViewImpl* web_view = SetupTest(file, client); |
| |
| DummyExceptionStateForTesting es; |
| |
| // Oilpan: see runTouchActionTest() comment why these are persistent |
| // references. |
| Persistent<Document> document = |
| static_cast<Document*>(web_view->MainFrameImpl()->GetDocument()); |
| Persistent<StaticElementList> host_nodes = |
| document->QuerySelectorAll("[shadow-host]", es); |
| ASSERT_FALSE(es.HadException()); |
| ASSERT_GE(host_nodes->length(), 1u); |
| |
| for (unsigned index = 0; index < host_nodes->length(); index++) { |
| ShadowRoot* shadow_root = host_nodes->item(index)->OpenShadowRoot(); |
| RunTestOnTree(shadow_root, web_view, client); |
| } |
| |
| // Projections show up in the main document. |
| RunTestOnTree(document.Get(), web_view, client); |
| |
| // Explicitly reset to break dependency on locally scoped client. |
| web_view_helper_.Reset(); |
| } |
| |
| void TouchActionTest::RunIFrameTest(std::string file) { |
| TouchActionTrackingWebWidgetClient client; |
| |
| WebViewImpl* web_view = SetupTest(file, client); |
| WebFrame* cur_frame = web_view->MainFrame()->FirstChild(); |
| ASSERT_TRUE(cur_frame); |
| |
| for (; cur_frame; cur_frame = cur_frame->NextSibling()) { |
| // Oilpan: see runTouchActionTest() comment why these are persistent |
| // references. |
| Persistent<Document> content_doc = |
| static_cast<Document*>(cur_frame->ToWebLocalFrame()->GetDocument()); |
| RunTestOnTree(content_doc.Get(), web_view, client); |
| } |
| |
| // Explicitly reset to break dependency on locally scoped client. |
| web_view_helper_.Reset(); |
| } |
| |
| WebViewImpl* TouchActionTest::SetupTest( |
| std::string file, |
| TouchActionTrackingWebWidgetClient& client) { |
| URLTestHelpers::RegisterMockedURLLoadFromBase(WebString::FromUTF8(base_url_), |
| test::CoreTestDataPath(), |
| WebString::FromUTF8(file)); |
| // Note that JavaScript must be enabled for shadow DOM tests. |
| WebViewImpl* web_view = web_view_helper_.InitializeAndLoad( |
| base_url_ + file, nullptr, nullptr, &client); |
| |
| // Set size to enable hit testing, and avoid line wrapping for consistency |
| // with browser. |
| web_view->Resize(WebSize(900, 1600)); |
| |
| // Scroll to verify the code properly transforms windows to client co-ords. |
| const int kScrollOffset = 100; |
| Document* document = |
| static_cast<Document*>(web_view->MainFrameImpl()->GetDocument()); |
| document->GetFrame()->View()->LayoutViewport()->SetScrollOffset( |
| ScrollOffset(0, kScrollOffset), kProgrammaticScroll); |
| |
| return web_view; |
| } |
| |
| IntRect WindowClipRect(const LocalFrameView& frame_view) { |
| LayoutRect clip_rect(LayoutPoint(), LayoutSize(frame_view.Size())); |
| frame_view.GetLayoutView()->MapToVisualRectInAncestorSpace( |
| &frame_view.GetLayoutView()->ContainerForPaintInvalidation(), clip_rect, |
| 0, kDefaultVisualRectFlags); |
| return EnclosingIntRect(clip_rect); |
| } |
| |
| void TouchActionTest::RunTestOnTree( |
| ContainerNode* root, |
| WebView* web_view, |
| TouchActionTrackingWebWidgetClient& client) { |
| // Find all elements to test the touch-action of in the document. |
| DummyExceptionStateForTesting es; |
| |
| // Oilpan: see runTouchActionTest() comment why these are persistent |
| // references. |
| Persistent<StaticElementList> elements = |
| root->QuerySelectorAll("[expected-action]", es); |
| ASSERT_FALSE(es.HadException()); |
| |
| for (unsigned index = 0; index < elements->length(); index++) { |
| Element* element = elements->item(index); |
| element->scrollIntoViewIfNeeded(); |
| |
| std::string failure_context("Test case: "); |
| if (element->HasID()) { |
| failure_context.append(element->GetIdAttribute().Ascii().data()); |
| } else if (element->firstChild()) { |
| failure_context.append("\""); |
| failure_context.append(element->firstChild() |
| ->textContent(false) |
| .StripWhiteSpace() |
| .Ascii() |
| .data()); |
| failure_context.append("\""); |
| } else { |
| failure_context += "<missing ID>"; |
| } |
| |
| // Run each test three times at different positions in the element. |
| // Note that we don't want the bounding box because our tests sometimes have |
| // elements with multiple border boxes with other elements in between. Use |
| // the first border box (which we can easily visualize in a browser for |
| // debugging). |
| Persistent<DOMRectList> rects = element->getClientRects(); |
| ASSERT_GE(rects->length(), 0u) << failure_context; |
| Persistent<DOMRect> r = rects->item(0); |
| FloatRect client_float_rect = |
| FloatRect(r->left(), r->top(), r->width(), r->height()); |
| IntRect client_rect = EnclosedIntRect(client_float_rect); |
| for (int loc_idx = 0; loc_idx < 3; loc_idx++) { |
| IntPoint frame_point; |
| std::stringstream context_stream; |
| context_stream << failure_context << " ("; |
| switch (loc_idx) { |
| case 0: |
| frame_point = client_rect.Center(); |
| context_stream << "center"; |
| break; |
| case 1: |
| frame_point = client_rect.Location(); |
| context_stream << "top-left"; |
| break; |
| case 2: |
| frame_point = client_rect.MaxXMaxYCorner(); |
| frame_point.Move(-1, -1); |
| context_stream << "bottom-right"; |
| break; |
| default: |
| FAIL() << "Invalid location index."; |
| } |
| |
| IntPoint window_point = |
| root->GetDocument().GetFrame()->View()->ConvertToRootFrame( |
| frame_point); |
| context_stream << "=" << window_point.X() << "," << window_point.Y() |
| << ")."; |
| std::string failure_context_pos = context_stream.str(); |
| |
| LocalFrame* main_frame = |
| ToLocalFrame(WebFrame::ToCoreFrame(*web_view->MainFrame())); |
| LocalFrameView* main_frame_view = main_frame->View(); |
| IntRect visible_rect = WindowClipRect(*main_frame_view); |
| ASSERT_TRUE(visible_rect.Contains(window_point)) |
| << failure_context_pos |
| << " Test point not contained in visible area: " << visible_rect.X() |
| << "," << visible_rect.Y() << "-" << visible_rect.MaxX() << "," |
| << visible_rect.MaxY(); |
| |
| // First validate that a hit test at this point will really hit the |
| // element we intended. This is the easiest way for a test to be broken, |
| // but has nothing really to do with touch action. Note that we can't use |
| // WebView's hit test API because it doesn't look into shadow DOM. |
| HitTestResult result = main_frame->GetEventHandler().HitTestResultAtPoint( |
| window_point, HitTestRequest::kReadOnly | HitTestRequest::kActive); |
| ASSERT_EQ(element, result.InnerElement()) |
| << "Unexpected hit test result " << failure_context_pos |
| << " Got element: \"" |
| << result.InnerElement() |
| ->OuterHTMLAsString() |
| .StripWhiteSpace() |
| .Left(80) |
| .Ascii() |
| .data() |
| << "\"" << std::endl |
| << "Document render tree:" << std::endl |
| << ExternalRepresentation(root->GetDocument().GetFrame()) |
| .Utf8() |
| .data(); |
| |
| // Now send the touch event and check any touch action result. |
| SendTouchEvent(web_view, WebInputEvent::kPointerDown, window_point); |
| |
| AtomicString expected_action = element->getAttribute("expected-action"); |
| // Should have received exactly one touch action, even for auto. |
| EXPECT_EQ(1, client.TouchActionSetCount()) << failure_context_pos; |
| if (client.TouchActionSetCount()) { |
| if (expected_action == "auto") { |
| EXPECT_EQ(TouchAction::kTouchActionAuto, client.LastTouchAction()) |
| << failure_context_pos; |
| } else if (expected_action == "none") { |
| EXPECT_EQ(TouchAction::kTouchActionNone, client.LastTouchAction()) |
| << failure_context_pos; |
| } else if (expected_action == "pan-x") { |
| EXPECT_EQ(TouchAction::kTouchActionPanX, client.LastTouchAction()) |
| << failure_context_pos; |
| } else if (expected_action == "pan-y") { |
| EXPECT_EQ(TouchAction::kTouchActionPanY, client.LastTouchAction()) |
| << failure_context_pos; |
| } else if (expected_action == "pan-x-y") { |
| EXPECT_EQ((TouchAction::kTouchActionPan), client.LastTouchAction()) |
| << failure_context_pos; |
| } else if (expected_action == "manipulation") { |
| EXPECT_EQ((TouchAction::kTouchActionManipulation), |
| client.LastTouchAction()) |
| << failure_context_pos; |
| } else { |
| FAIL() << "Unrecognized expected-action \"" |
| << expected_action.Ascii().data() << "\" " |
| << failure_context_pos; |
| } |
| } |
| |
| // Reset webview touch state. |
| client.Reset(); |
| SendTouchEvent(web_view, WebInputEvent::kPointerCancel, window_point); |
| EXPECT_EQ(0, client.TouchActionSetCount()); |
| } |
| } |
| } |
| void TouchActionTest::SendTouchEvent(WebView* web_view, |
| WebInputEvent::Type type, |
| IntPoint client_point) { |
| ASSERT_TRUE(type == WebInputEvent::kPointerDown || |
| type == WebInputEvent::kPointerCancel); |
| |
| WebPointerEvent event( |
| type, |
| WebPointerProperties(1, WebPointerProperties::PointerType::kTouch, |
| WebPointerProperties::Button::kLeft, |
| WebFloatPoint(client_point.X(), client_point.Y()), |
| WebFloatPoint(client_point.X(), client_point.Y())), |
| 10.0f, 10.0f); |
| if (type == WebInputEvent::kPointerCancel) |
| event.dispatch_type = WebInputEvent::kEventNonBlocking; |
| |
| web_view->HandleInputEvent(WebCoalescedInputEvent(event)); |
| web_view->DispatchBufferedTouchEvents(); |
| RunPendingTasks(); |
| } |
| |
| // crbug.com/411038 |
| TEST_F(TouchActionTest, Simple) { |
| RunTouchActionTest("touch-action-simple.html"); |
| } |
| |
| TEST_F(TouchActionTest, Overflow) { |
| RunTouchActionTest("touch-action-overflow.html"); |
| } |
| |
| TEST_F(TouchActionTest, IFrame) { |
| RunIFrameTest("touch-action-iframe.html"); |
| } |
| |
| TEST_F(TouchActionTest, ShadowDOM) { |
| RunShadowDOMTest("touch-action-shadow-dom.html"); |
| } |
| |
| TEST_F(TouchActionTest, Pan) { |
| RunTouchActionTest("touch-action-pan.html"); |
| } |
| |
| } // namespace blink |