|  | // Copyright (c) 2012 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 <tuple> | 
|  |  | 
|  | #include "base/macros.h" | 
|  | #include "base/strings/utf_string_conversions.h" | 
|  | #include "base/time/time.h" | 
|  | #include "build/build_config.h" | 
|  | #include "content/common/accessibility_messages.h" | 
|  | #include "content/common/frame_messages.h" | 
|  | #include "content/common/site_isolation_policy.h" | 
|  | #include "content/common/view_message_enums.h" | 
|  | #include "content/public/common/content_switches.h" | 
|  | #include "content/public/test/render_view_test.h" | 
|  | #include "content/renderer/accessibility/render_accessibility_impl.h" | 
|  | #include "content/renderer/render_frame_impl.h" | 
|  | #include "content/renderer/render_view_impl.h" | 
|  | #include "testing/gtest/include/gtest/gtest.h" | 
|  | #include "third_party/WebKit/public/platform/WebSize.h" | 
|  | #include "third_party/WebKit/public/web/WebAXObject.h" | 
|  | #include "third_party/WebKit/public/web/WebDocument.h" | 
|  | #include "third_party/WebKit/public/web/WebView.h" | 
|  | #include "ui/accessibility/ax_node_data.h" | 
|  |  | 
|  | using blink::WebAXObject; | 
|  | using blink::WebDocument; | 
|  |  | 
|  | namespace content { | 
|  |  | 
|  | class TestRenderAccessibilityImpl : public RenderAccessibilityImpl { | 
|  | public: | 
|  | explicit TestRenderAccessibilityImpl(RenderFrameImpl* render_frame) | 
|  | : RenderAccessibilityImpl(render_frame) { | 
|  | } | 
|  |  | 
|  | void SendPendingAccessibilityEvents() { | 
|  | RenderAccessibilityImpl::SendPendingAccessibilityEvents(); | 
|  | } | 
|  | }; | 
|  |  | 
|  | class RenderAccessibilityImplTest : public RenderViewTest { | 
|  | public: | 
|  | RenderAccessibilityImplTest() {} | 
|  |  | 
|  | RenderViewImpl* view() { | 
|  | return static_cast<RenderViewImpl*>(view_); | 
|  | } | 
|  |  | 
|  | RenderFrameImpl* frame() { | 
|  | return static_cast<RenderFrameImpl*>(view()->GetMainRenderFrame()); | 
|  | } | 
|  |  | 
|  | void SetUp() override { | 
|  | RenderViewTest::SetUp(); | 
|  | sink_ = &render_thread_->sink(); | 
|  | } | 
|  |  | 
|  | void TearDown() override { | 
|  | #if defined(LEAK_SANITIZER) | 
|  | // Do this before shutting down V8 in RenderViewTest::TearDown(). | 
|  | // http://crbug.com/328552 | 
|  | __lsan_do_leak_check(); | 
|  | #endif | 
|  | RenderViewTest::TearDown(); | 
|  | } | 
|  |  | 
|  | void SetMode(AccessibilityMode mode) { | 
|  | frame()->OnSetAccessibilityMode(mode); | 
|  | } | 
|  |  | 
|  | void GetAllAccEvents( | 
|  | std::vector<AccessibilityHostMsg_EventParams>* param_list) { | 
|  | const IPC::Message* message = | 
|  | sink_->GetUniqueMessageMatching(AccessibilityHostMsg_Events::ID); | 
|  | ASSERT_TRUE(message); | 
|  | std::tuple<std::vector<AccessibilityHostMsg_EventParams>, int> param; | 
|  | AccessibilityHostMsg_Events::Read(message, ¶m); | 
|  | *param_list = std::get<0>(param); | 
|  | } | 
|  |  | 
|  | void GetLastAccEvent( | 
|  | AccessibilityHostMsg_EventParams* params) { | 
|  | std::vector<AccessibilityHostMsg_EventParams> param_list; | 
|  | GetAllAccEvents(¶m_list); | 
|  | ASSERT_GE(param_list.size(), 1U); | 
|  | *params = param_list[0]; | 
|  | } | 
|  |  | 
|  | int CountAccessibilityNodesSentToBrowser() { | 
|  | AccessibilityHostMsg_EventParams event; | 
|  | GetLastAccEvent(&event); | 
|  | return event.update.nodes.size(); | 
|  | } | 
|  |  | 
|  | protected: | 
|  | IPC::TestSink* sink_; | 
|  |  | 
|  | DISALLOW_COPY_AND_ASSIGN(RenderAccessibilityImplTest); | 
|  |  | 
|  | }; | 
|  |  | 
|  | TEST_F(RenderAccessibilityImplTest, SendFullAccessibilityTreeOnReload) { | 
|  | // The job of RenderAccessibilityImpl is to serialize the | 
|  | // accessibility tree built by WebKit and send it to the browser. | 
|  | // When the accessibility tree changes, it tries to send only | 
|  | // the nodes that actually changed or were reparented. This test | 
|  | // ensures that the messages sent are correct in cases when a page | 
|  | // reloads, and that internal state is properly garbage-collected. | 
|  | std::string html = | 
|  | "<body>" | 
|  | "  <div role='group' id='A'>" | 
|  | "    <div role='group' id='A1'></div>" | 
|  | "    <div role='group' id='A2'></div>" | 
|  | "  </div>" | 
|  | "</body>"; | 
|  | LoadHTML(html.c_str()); | 
|  |  | 
|  | // Creating a RenderAccessibilityImpl should sent the tree to the browser. | 
|  | std::unique_ptr<TestRenderAccessibilityImpl> accessibility( | 
|  | new TestRenderAccessibilityImpl(frame())); | 
|  | accessibility->SendPendingAccessibilityEvents(); | 
|  | EXPECT_EQ(4, CountAccessibilityNodesSentToBrowser()); | 
|  |  | 
|  | // If we post another event but the tree doesn't change, | 
|  | // we should only send 1 node to the browser. | 
|  | sink_->ClearMessages(); | 
|  | WebDocument document = view()->GetWebView()->mainFrame()->document(); | 
|  | WebAXObject root_obj = document.accessibilityObject(); | 
|  | accessibility->HandleAXEvent( | 
|  | root_obj, | 
|  | ui::AX_EVENT_LAYOUT_COMPLETE); | 
|  | accessibility->SendPendingAccessibilityEvents(); | 
|  | EXPECT_EQ(1, CountAccessibilityNodesSentToBrowser()); | 
|  | { | 
|  | // Make sure it's the root object that was updated. | 
|  | AccessibilityHostMsg_EventParams event; | 
|  | GetLastAccEvent(&event); | 
|  | EXPECT_EQ(root_obj.axID(), event.update.nodes[0].id); | 
|  | } | 
|  |  | 
|  | // If we reload the page and send a event, we should send | 
|  | // all 4 nodes to the browser. Also double-check that we didn't | 
|  | // leak any of the old BrowserTreeNodes. | 
|  | LoadHTML(html.c_str()); | 
|  | document = view()->GetWebView()->mainFrame()->document(); | 
|  | root_obj = document.accessibilityObject(); | 
|  | sink_->ClearMessages(); | 
|  | accessibility->HandleAXEvent( | 
|  | root_obj, | 
|  | ui::AX_EVENT_LAYOUT_COMPLETE); | 
|  | accessibility->SendPendingAccessibilityEvents(); | 
|  | EXPECT_EQ(4, CountAccessibilityNodesSentToBrowser()); | 
|  |  | 
|  | // Even if the first event is sent on an element other than | 
|  | // the root, the whole tree should be updated because we know | 
|  | // the browser doesn't have the root element. | 
|  | LoadHTML(html.c_str()); | 
|  | document = view()->GetWebView()->mainFrame()->document(); | 
|  | root_obj = document.accessibilityObject(); | 
|  | sink_->ClearMessages(); | 
|  | const WebAXObject& first_child = root_obj.childAt(0); | 
|  | accessibility->HandleAXEvent( | 
|  | first_child, | 
|  | ui::AX_EVENT_LIVE_REGION_CHANGED); | 
|  | accessibility->SendPendingAccessibilityEvents(); | 
|  | EXPECT_EQ(4, CountAccessibilityNodesSentToBrowser()); | 
|  | } | 
|  |  | 
|  | TEST_F(RenderAccessibilityImplTest, HideAccessibilityObject) { | 
|  | // Test RenderAccessibilityImpl and make sure it sends the | 
|  | // proper event to the browser when an object in the tree | 
|  | // is hidden, but its children are not. | 
|  | std::string html = | 
|  | "<body>" | 
|  | "  <div role='group' id='A'>" | 
|  | "    <div role='group' id='B'>" | 
|  | "      <div role='group' id='C' style='visibility:visible'>" | 
|  | "      </div>" | 
|  | "    </div>" | 
|  | "  </div>" | 
|  | "</body>"; | 
|  | LoadHTML(html.c_str()); | 
|  |  | 
|  | std::unique_ptr<TestRenderAccessibilityImpl> accessibility( | 
|  | new TestRenderAccessibilityImpl(frame())); | 
|  | accessibility->SendPendingAccessibilityEvents(); | 
|  | EXPECT_EQ(4, CountAccessibilityNodesSentToBrowser()); | 
|  |  | 
|  | WebDocument document = view()->GetWebView()->mainFrame()->document(); | 
|  | WebAXObject root_obj = document.accessibilityObject(); | 
|  | WebAXObject node_a = root_obj.childAt(0); | 
|  | WebAXObject node_b = node_a.childAt(0); | 
|  | WebAXObject node_c = node_b.childAt(0); | 
|  |  | 
|  | // Hide node 'B' ('C' stays visible). | 
|  | ExecuteJavaScriptForTests( | 
|  | "document.getElementById('B').style.visibility = 'hidden';"); | 
|  | // Force layout now. | 
|  | ExecuteJavaScriptForTests("document.getElementById('B').offsetLeft;"); | 
|  |  | 
|  | // Send a childrenChanged on 'A'. | 
|  | sink_->ClearMessages(); | 
|  | accessibility->HandleAXEvent( | 
|  | node_a, | 
|  | ui::AX_EVENT_CHILDREN_CHANGED); | 
|  |  | 
|  | accessibility->SendPendingAccessibilityEvents(); | 
|  | AccessibilityHostMsg_EventParams event; | 
|  | GetLastAccEvent(&event); | 
|  | ASSERT_EQ(2U, event.update.nodes.size()); | 
|  |  | 
|  | // RenderAccessibilityImpl notices that 'C' is being reparented, | 
|  | // so it clears the subtree rooted at 'A', then updates 'A' and then 'C'. | 
|  | EXPECT_EQ(node_a.axID(), event.update.node_id_to_clear); | 
|  | EXPECT_EQ(node_a.axID(), event.update.nodes[0].id); | 
|  | EXPECT_EQ(node_c.axID(), event.update.nodes[1].id); | 
|  | EXPECT_EQ(2, CountAccessibilityNodesSentToBrowser()); | 
|  | } | 
|  |  | 
|  | TEST_F(RenderAccessibilityImplTest, ShowAccessibilityObject) { | 
|  | // Test RenderAccessibilityImpl and make sure it sends the | 
|  | // proper event to the browser when an object in the tree | 
|  | // is shown, causing its own already-visible children to be | 
|  | // reparented to it. | 
|  | std::string html = | 
|  | "<body>" | 
|  | "  <div role='group' id='A'>" | 
|  | "    <div role='group' id='B' style='visibility:hidden'>" | 
|  | "      <div role='group' id='C' style='visibility:visible'>" | 
|  | "      </div>" | 
|  | "    </div>" | 
|  | "  </div>" | 
|  | "</body>"; | 
|  | LoadHTML(html.c_str()); | 
|  |  | 
|  | std::unique_ptr<TestRenderAccessibilityImpl> accessibility( | 
|  | new TestRenderAccessibilityImpl(frame())); | 
|  | accessibility->SendPendingAccessibilityEvents(); | 
|  | EXPECT_EQ(3, CountAccessibilityNodesSentToBrowser()); | 
|  |  | 
|  | // Show node 'B', then send a childrenChanged on 'A'. | 
|  | ExecuteJavaScriptForTests( | 
|  | "document.getElementById('B').style.visibility = 'visible';"); | 
|  | ExecuteJavaScriptForTests("document.getElementById('B').offsetLeft;"); | 
|  |  | 
|  | sink_->ClearMessages(); | 
|  | WebDocument document = view()->GetWebView()->mainFrame()->document(); | 
|  | WebAXObject root_obj = document.accessibilityObject(); | 
|  | WebAXObject node_a = root_obj.childAt(0); | 
|  | WebAXObject node_b = node_a.childAt(0); | 
|  | WebAXObject node_c = node_b.childAt(0); | 
|  |  | 
|  | accessibility->HandleAXEvent( | 
|  | node_a, | 
|  | ui::AX_EVENT_CHILDREN_CHANGED); | 
|  |  | 
|  | accessibility->SendPendingAccessibilityEvents(); | 
|  | AccessibilityHostMsg_EventParams event; | 
|  | GetLastAccEvent(&event); | 
|  |  | 
|  | ASSERT_EQ(3U, event.update.nodes.size()); | 
|  | EXPECT_EQ(node_a.axID(), event.update.node_id_to_clear); | 
|  | EXPECT_EQ(node_a.axID(), event.update.nodes[0].id); | 
|  | EXPECT_EQ(node_b.axID(), event.update.nodes[1].id); | 
|  | EXPECT_EQ(node_c.axID(), event.update.nodes[2].id); | 
|  | EXPECT_EQ(3, CountAccessibilityNodesSentToBrowser()); | 
|  | } | 
|  |  | 
|  | TEST_F(RenderAccessibilityImplTest, DetachAccessibilityObject) { | 
|  | // Test RenderAccessibilityImpl and make sure it sends the | 
|  | // proper event to the browser when an object in the tree | 
|  | // is detached, but its children are not. This can happen when | 
|  | // a layout occurs and an anonymous render block is no longer needed. | 
|  | std::string html = | 
|  | "<body aria-label='Body'>" | 
|  | "<span>1</span><span style='display:block'>2</span>" | 
|  | "</body>"; | 
|  | LoadHTML(html.c_str()); | 
|  |  | 
|  | std::unique_ptr<TestRenderAccessibilityImpl> accessibility( | 
|  | new TestRenderAccessibilityImpl(frame())); | 
|  | accessibility->SendPendingAccessibilityEvents(); | 
|  | EXPECT_EQ(7, CountAccessibilityNodesSentToBrowser()); | 
|  |  | 
|  | // Initially, the accessibility tree looks like this: | 
|  | // | 
|  | //   Document | 
|  | //   +--Body | 
|  | //      +--Anonymous Block | 
|  | //         +--Static Text "1" | 
|  | //            +--Inline Text Box "1" | 
|  | //      +--Static Text "2" | 
|  | //         +--Inline Text Box "2" | 
|  | WebDocument document = view()->GetWebView()->mainFrame()->document(); | 
|  | WebAXObject root_obj = document.accessibilityObject(); | 
|  | WebAXObject body = root_obj.childAt(0); | 
|  | WebAXObject anonymous_block = body.childAt(0); | 
|  | WebAXObject text_1 = anonymous_block.childAt(0); | 
|  | WebAXObject text_2 = body.childAt(1); | 
|  |  | 
|  | // Change the display of the second 'span' back to inline, which causes the | 
|  | // anonymous block to be destroyed. | 
|  | ExecuteJavaScriptForTests( | 
|  | "document.querySelectorAll('span')[1].style.display = 'inline';"); | 
|  | // Force layout now. | 
|  | ExecuteJavaScriptForTests("document.body.offsetLeft;"); | 
|  |  | 
|  | // Send a childrenChanged on the body. | 
|  | sink_->ClearMessages(); | 
|  | accessibility->HandleAXEvent( | 
|  | body, | 
|  | ui::AX_EVENT_CHILDREN_CHANGED); | 
|  |  | 
|  | accessibility->SendPendingAccessibilityEvents(); | 
|  |  | 
|  | // Afterwards, the accessibility tree looks like this: | 
|  | // | 
|  | //   Document | 
|  | //   +--Body | 
|  | //      +--Static Text "1" | 
|  | //         +--Inline Text Box "1" | 
|  | //      +--Static Text "2" | 
|  | //         +--Inline Text Box "2" | 
|  | // | 
|  | // We just assert that there are now four nodes in the | 
|  | // accessibility tree and that only three nodes needed | 
|  | // to be updated (the body, the static text 1, and | 
|  | // the static text 2). | 
|  |  | 
|  | AccessibilityHostMsg_EventParams event; | 
|  | GetLastAccEvent(&event); | 
|  | ASSERT_EQ(5U, event.update.nodes.size()); | 
|  |  | 
|  | EXPECT_EQ(body.axID(), event.update.nodes[0].id); | 
|  | EXPECT_EQ(text_1.axID(), event.update.nodes[1].id); | 
|  | // The third event is to update text_2, but its id changes | 
|  | // so we don't have a test expectation for it. | 
|  | } | 
|  |  | 
|  | }  // namespace content |