| // Copyright 2019 The Fuchsia 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 <fuchsia/ui/input/accessibility/cpp/fidl.h> |
| #include <fuchsia/ui/input/cpp/fidl.h> |
| #include <fuchsia/ui/views/cpp/fidl.h> |
| #include <lib/ui/scenic/cpp/resources.h> |
| #include <lib/ui/scenic/cpp/session.h> |
| #include <lib/ui/scenic/cpp/view_ref_pair.h> |
| #include <lib/ui/scenic/cpp/view_token_pair.h> |
| |
| #include <limits> |
| #include <memory> |
| #include <vector> |
| |
| #include <gtest/gtest.h> |
| |
| #include "src/ui/scenic/lib/gfx/engine/view_tree.h" |
| #include "src/ui/scenic/lib/input/input_system.h" |
| #include "src/ui/scenic/lib/input/tests/util.h" |
| #include "src/ui/scenic/lib/utils/helpers.h" |
| |
| namespace lib_ui_input_tests { |
| namespace { |
| |
| // common test setups: |
| // |
| // In each test case, a basic Scenic scene will be created, as well as a client with a view. The |
| // test will also register an accessibility listener with the input system. Tests may then exercise |
| // the injection of pointer events into the session. Depending on the accessibility listener |
| // response, configured with client.SetResponses(...), the pointer events will be consumed / |
| // rejected. When they are consumed, the view should not receive any events. When they are |
| // rejected, it should. |
| |
| using AccessibilityPointerEvent = fuchsia::ui::input::accessibility::PointerEvent; |
| using InputCommand = fuchsia::ui::input::Command; |
| using Phase = fuchsia::ui::input::PointerEventPhase; |
| using fuchsia::ui::input::InputEvent; |
| using fuchsia::ui::input::PointerEvent; |
| using fuchsia::ui::input::PointerEventType; |
| using fuchsia::ui::views::ViewHolderToken; |
| using fuchsia::ui::views::ViewToken; |
| using scenic_impl::gfx::ViewTree; |
| |
| constexpr float kNdcEpsilon = std::numeric_limits<float>::epsilon(); |
| |
| class MockAccessibilityPointerEventListener |
| : public fuchsia::ui::input::accessibility::PointerEventListener { |
| public: |
| MockAccessibilityPointerEventListener(scenic_impl::input::InputSystem* input) : binding_(this) { |
| binding_.set_error_handler([this](zx_status_t) { is_registered_ = false; }); |
| input->RegisterA11yListener(binding_.NewBinding(), |
| [this](bool success) { is_registered_ = success; }); |
| } |
| |
| bool is_registered() const { return is_registered_; } |
| std::vector<fuchsia::ui::input::accessibility::PointerEvent>& events() { return events_; } |
| const std::vector<fuchsia::ui::input::accessibility::PointerEvent>& events() const { |
| return events_; |
| } |
| |
| // Configures how this mock will answer to incoming events. |
| // |
| // |responses| is a vector, where each pair contains the number of events that |
| // will be seen before it responds with an EventHandling value. |
| void SetResponses( |
| std::vector<std::pair<uint32_t, fuchsia::ui::input::accessibility::EventHandling>> |
| responses) { |
| responses_ = std::move(responses); |
| } |
| |
| private: |
| // |fuchsia::ui::input::accessibility::AccessibilityPointerEventListener| |
| // Performs a response, and resets for the next response. |
| void OnEvent(fuchsia::ui::input::accessibility::PointerEvent pointer_event) override { |
| events_.emplace_back(std::move(pointer_event)); |
| ++num_events_until_response_; |
| if (!responses_.empty() && num_events_until_response_ == responses_.front().first) { |
| num_events_until_response_ = 0; |
| binding_.events().OnStreamHandled( |
| /*device_id=*/1, /*pointer_id=*/1, |
| /*handled=*/responses_.front().second); |
| responses_.erase(responses_.begin()); |
| } |
| } |
| |
| fidl::Binding<fuchsia::ui::input::accessibility::PointerEventListener> binding_; |
| bool is_registered_ = false; |
| // See |SetResponses|. |
| std::vector<std::pair<uint32_t, fuchsia::ui::input::accessibility::EventHandling>> responses_; |
| |
| std::vector<fuchsia::ui::input::accessibility::PointerEvent> events_; |
| uint32_t num_events_until_response_ = 0; |
| }; |
| |
| // Setup common to most of the tests in this suite, which set up a single child view. |
| struct SingleChildViewSetup { |
| SessionWrapper root_view; |
| SessionWrapper child_view; |
| const uint32_t compositor_id; |
| }; |
| |
| // Setup with two nested child views, for testing injection that requires context and target views. |
| struct TwoChildViewSetup { |
| SessionWrapper root_view; |
| SessionWrapper child_view; |
| SessionWrapper child_view2; |
| const uint32_t compositor_id; |
| }; |
| |
| // Test fixture that sets up a 5x5 "display" and has utilities to wire up views with view refs for |
| // Accessibility. |
| class AccessibilityPointerEventsTest : public InputSystemTest { |
| protected: |
| uint32_t test_display_width_px() const override { return 5; } |
| uint32_t test_display_height_px() const override { return 5; } |
| |
| // Most of the tests in this suite set up a single view. |
| SingleChildViewSetup SetUpSingleView(const fuchsia::ui::gfx::ViewProperties& view_properties) { |
| auto [root_view, root_resources] = CreateScene(); |
| auto [view_token, view_holder_token] = scenic::ViewTokenPair::New(); |
| { |
| scenic::ViewHolder view_holder(root_view.session(), std::move(view_holder_token), |
| "View Holder"); |
| view_holder.SetViewProperties(view_properties); |
| root_view.view()->AddChild(view_holder); |
| } |
| |
| RequestToPresent(root_view.session()); |
| |
| return { |
| std::move(root_view), |
| CreateClient("a11y-single-view", std::move(view_token)), |
| root_resources.compositor.id(), |
| }; |
| } |
| |
| TwoChildViewSetup SetUpTwoViews(const fuchsia::ui::gfx::ViewProperties& view_properties) { |
| SingleChildViewSetup single_view_scene = SetUpSingleView(view_properties); |
| |
| auto [view_token2, view_holder_token2] = scenic::ViewTokenPair::New(); |
| { |
| scenic::ViewHolder view_holder(single_view_scene.child_view.session(), |
| std::move(view_holder_token2), "View Holder"); |
| view_holder.SetViewProperties(view_properties); |
| single_view_scene.child_view.view()->AddChild(view_holder); |
| } |
| |
| RequestToPresent(single_view_scene.child_view.session()); |
| |
| return { |
| .root_view = std::move(single_view_scene.root_view), |
| .child_view = std::move(single_view_scene.child_view), |
| .child_view2 = CreateClient("a11y-second-view", std::move(view_token2)), |
| .compositor_id = single_view_scene.compositor_id, |
| }; |
| } |
| }; |
| |
| // This test makes sure that first to register win is working. |
| TEST_F(AccessibilityPointerEventsTest, RegistersAccessibilityListenerOnlyOnce) { |
| MockAccessibilityPointerEventListener listener_1(input_system()); |
| RunLoopUntilIdle(); |
| |
| EXPECT_TRUE(listener_1.is_registered()); |
| |
| MockAccessibilityPointerEventListener listener_2(input_system()); |
| RunLoopUntilIdle(); |
| |
| EXPECT_FALSE(listener_2.is_registered()) << "The second listener that attempts to connect should " |
| "fail, as there is already one connected."; |
| EXPECT_TRUE(listener_1.is_registered()) << "First listener should still be connected."; |
| } |
| |
| // In this test two pointer event streams will be injected in the input system. The first one, with |
| // four pointer events, will be accepted in the second pointer event. The second one, also with four |
| // pointer events, will be accepted in the fourth one. |
| TEST_F(AccessibilityPointerEventsTest, ConsumesPointerEvents) { |
| auto [root_view, view, compositor_id] = SetUpSingleView(k5x5x1); |
| |
| MockAccessibilityPointerEventListener listener(input_system()); |
| listener.SetResponses({ |
| {2, fuchsia::ui::input::accessibility::EventHandling::CONSUMED}, |
| {6, fuchsia::ui::input::accessibility::EventHandling::CONSUMED}, |
| }); |
| |
| // Scene is now set up; send in the input. |
| { |
| scenic::Session* const session = root_view.session(); |
| PointerCommandGenerator pointer(compositor_id, /*device id*/ 1, |
| /*pointer id*/ 1, PointerEventType::TOUCH); |
| // A touch sequence that starts at the (2.5,2.5) location of the 5x5 display. |
| session->Enqueue(pointer.Add(2.5, 2.5)); |
| session->Enqueue(pointer.Down(2.5, 2.5)); // Consume happens here. |
| } |
| RunLoopUntilIdle(); |
| |
| // Verify view's events. |
| EXPECT_TRUE(view.events().empty()) |
| << "View should not receive events until Accessibility allows it."; |
| |
| // Verify accessibility's events. |
| { |
| const std::vector<AccessibilityPointerEvent>& events = listener.events(); |
| EXPECT_EQ(events.size(), 2u); |
| // ADD |
| { |
| const AccessibilityPointerEvent& add = events[0]; |
| EXPECT_EQ(add.phase(), Phase::ADD); |
| EXPECT_EQ(add.ndc_point().x, 0); |
| EXPECT_EQ(add.ndc_point().y, 0); |
| EXPECT_EQ(add.viewref_koid(), view.ViewKoid()); |
| EXPECT_EQ(add.local_point().x, 2.5); |
| EXPECT_EQ(add.local_point().y, 2.5); |
| } |
| |
| // DOWN |
| { |
| const AccessibilityPointerEvent& down = events[1]; |
| EXPECT_EQ(down.phase(), Phase::DOWN); |
| EXPECT_EQ(down.ndc_point().x, 0); |
| EXPECT_EQ(down.ndc_point().y, 0); |
| EXPECT_EQ(down.viewref_koid(), view.ViewKoid()); |
| EXPECT_EQ(down.local_point().x, 2.5); |
| EXPECT_EQ(down.local_point().y, 2.5); |
| } |
| } |
| |
| view.events().clear(); |
| listener.events().clear(); |
| |
| // Accessibility consumed the two events. Continue sending pointer events in the same stream (a |
| // phase == REMOVE hasn't came yet, so they are part of the same stream). |
| { |
| scenic::Session* const session = root_view.session(); |
| PointerCommandGenerator pointer(compositor_id, /*device id*/ 1, |
| /*pointer id*/ 1, PointerEventType::TOUCH); |
| session->Enqueue(pointer.Up(2.5, 3.5)); |
| session->Enqueue(pointer.Remove(2.5, 3.5)); |
| } |
| RunLoopUntilIdle(); |
| |
| // Verify view's events. |
| EXPECT_TRUE(view.events().empty()) << "Accessibility should be consuming all events in this " |
| "stream; view should not be seeing them."; |
| |
| // Verify accessibility's events. |
| { |
| const std::vector<AccessibilityPointerEvent>& events = listener.events(); |
| EXPECT_EQ(events.size(), 2u); |
| // UP |
| { |
| const AccessibilityPointerEvent& up = events[0]; |
| EXPECT_EQ(up.phase(), Phase::UP); |
| EXPECT_EQ(up.ndc_point().x, 0); |
| EXPECT_NEAR(up.ndc_point().y, .4, kNdcEpsilon); |
| EXPECT_EQ(up.viewref_koid(), view.ViewKoid()); |
| EXPECT_EQ(up.local_point().x, 2.5); |
| EXPECT_EQ(up.local_point().y, 3.5); |
| } |
| |
| // REMOVE |
| { |
| const AccessibilityPointerEvent& remove = events[1]; |
| EXPECT_EQ(remove.phase(), Phase::REMOVE); |
| EXPECT_EQ(remove.ndc_point().x, 0); |
| EXPECT_NEAR(remove.ndc_point().y, .4, kNdcEpsilon); |
| EXPECT_EQ(remove.viewref_koid(), view.ViewKoid()); |
| EXPECT_EQ(remove.local_point().x, 2.5); |
| EXPECT_EQ(remove.local_point().y, 3.5); |
| } |
| } |
| |
| view.events().clear(); |
| listener.events().clear(); |
| |
| // Now, sends an entire stream at once. |
| { |
| scenic::Session* const session = root_view.session(); |
| PointerCommandGenerator pointer(compositor_id, /*device id*/ 1, |
| /*pointer id*/ 1, PointerEventType::TOUCH); |
| session->Enqueue(pointer.Add(3.5, 1.5)); |
| session->Enqueue(pointer.Down(3.5, 1.5)); |
| session->Enqueue(pointer.Up(3.5, 1.5)); |
| session->Enqueue(pointer.Remove(3.5, 1.5)); // Consume happens here. |
| } |
| RunLoopUntilIdle(); |
| |
| // Verify view's events. |
| EXPECT_TRUE(view.events().empty()) << "Accessibility should have consumed all events in the " |
| "stream; view should not have seen them."; |
| |
| // Verify accessibility's events. |
| { |
| const std::vector<AccessibilityPointerEvent>& events = listener.events(); |
| EXPECT_EQ(events.size(), 4u); |
| // ADD |
| { |
| const AccessibilityPointerEvent& add = events[0]; |
| EXPECT_EQ(add.phase(), Phase::ADD); |
| EXPECT_NEAR(add.ndc_point().x, .4, kNdcEpsilon); |
| EXPECT_NEAR(add.ndc_point().y, -.4, kNdcEpsilon); |
| EXPECT_EQ(add.viewref_koid(), view.ViewKoid()); |
| EXPECT_EQ(add.local_point().x, 3.5); |
| EXPECT_EQ(add.local_point().y, 1.5); |
| } |
| |
| // DOWN |
| { |
| const AccessibilityPointerEvent& down = events[1]; |
| EXPECT_EQ(down.phase(), Phase::DOWN); |
| EXPECT_NEAR(down.ndc_point().x, .4, kNdcEpsilon); |
| EXPECT_NEAR(down.ndc_point().y, -.4, kNdcEpsilon); |
| EXPECT_EQ(down.viewref_koid(), view.ViewKoid()); |
| EXPECT_EQ(down.local_point().x, 3.5); |
| EXPECT_EQ(down.local_point().y, 1.5); |
| } |
| |
| // UP |
| { |
| const AccessibilityPointerEvent& up = events[2]; |
| EXPECT_EQ(up.phase(), Phase::UP); |
| EXPECT_NEAR(up.ndc_point().x, .4, kNdcEpsilon); |
| EXPECT_NEAR(up.ndc_point().y, -.4, kNdcEpsilon); |
| EXPECT_EQ(up.viewref_koid(), view.ViewKoid()); |
| EXPECT_EQ(up.local_point().x, 3.5); |
| EXPECT_EQ(up.local_point().y, 1.5); |
| } |
| |
| // REMOVE |
| { |
| const AccessibilityPointerEvent& remove = events[3]; |
| EXPECT_EQ(remove.phase(), Phase::REMOVE); |
| EXPECT_NEAR(remove.ndc_point().x, .4, kNdcEpsilon); |
| EXPECT_NEAR(remove.ndc_point().y, -.4, kNdcEpsilon); |
| EXPECT_EQ(remove.viewref_koid(), view.ViewKoid()); |
| EXPECT_EQ(remove.local_point().x, 3.5); |
| EXPECT_EQ(remove.local_point().y, 1.5); |
| } |
| } |
| } |
| |
| // One pointer stream is injected in the input system. The listener rejects the pointer event. this |
| // test makes sure that buffered (past), as well as future pointer events are sent to the view. |
| TEST_F(AccessibilityPointerEventsTest, RejectsPointerEvents) { |
| auto [root_view, view, compositor_id] = SetUpSingleView(k5x5x1); |
| |
| MockAccessibilityPointerEventListener listener(input_system()); |
| listener.SetResponses({{2, fuchsia::ui::input::accessibility::EventHandling::REJECTED}}); |
| |
| // Scene is now set up; send in the input. |
| { |
| scenic::Session* const session = root_view.session(); |
| PointerCommandGenerator pointer(compositor_id, /*device id*/ 1, |
| /*pointer id*/ 1, PointerEventType::TOUCH); |
| // A touch sequence that starts at the (2.5,2.5) location of the 5x5 display. |
| session->Enqueue(pointer.Add(2.5, 2.5)); |
| session->Enqueue(pointer.Down(2.5, 2.5)); // Reject happens here. |
| } |
| RunLoopUntilIdle(); |
| |
| // Verify view's events. |
| { |
| const std::vector<InputEvent>& events = view.events(); |
| EXPECT_EQ(events.size(), 3u); |
| |
| // ADD |
| { |
| EXPECT_TRUE(events[0].is_pointer()); |
| const PointerEvent& add = events[0].pointer(); |
| EXPECT_EQ(add.x, 2.5); |
| EXPECT_EQ(add.y, 2.5); |
| } |
| |
| // FOCUS |
| EXPECT_TRUE(events[1].is_focus()); |
| |
| // DOWN |
| { |
| EXPECT_TRUE(events[2].is_pointer()); |
| const PointerEvent& down = events[2].pointer(); |
| EXPECT_EQ(down.x, 2.5); |
| EXPECT_EQ(down.y, 2.5); |
| } |
| } |
| |
| // Verify accessibility's events. Note that the listener must see two events here, but not later, |
| // because it rejects the stream in the second pointer event. |
| { |
| const std::vector<AccessibilityPointerEvent>& events = listener.events(); |
| EXPECT_EQ(events.size(), 2u); |
| // ADD |
| { |
| const AccessibilityPointerEvent& add = events[0]; |
| EXPECT_EQ(add.phase(), Phase::ADD); |
| EXPECT_EQ(add.ndc_point().x, 0); |
| EXPECT_EQ(add.ndc_point().y, 0); |
| EXPECT_EQ(add.viewref_koid(), view.ViewKoid()); |
| EXPECT_EQ(add.local_point().x, 2.5); |
| EXPECT_EQ(add.local_point().y, 2.5); |
| } |
| |
| // DOWN |
| { |
| const AccessibilityPointerEvent& down = events[1]; |
| EXPECT_EQ(down.phase(), Phase::DOWN); |
| EXPECT_EQ(down.ndc_point().x, 0); |
| EXPECT_EQ(down.ndc_point().y, 0); |
| EXPECT_EQ(down.viewref_koid(), view.ViewKoid()); |
| EXPECT_EQ(down.local_point().x, 2.5); |
| EXPECT_EQ(down.local_point().y, 2.5); |
| } |
| } |
| |
| view.events().clear(); |
| listener.events().clear(); |
| |
| // Send the rest of the stream. |
| { |
| scenic::Session* const session = root_view.session(); |
| PointerCommandGenerator pointer(compositor_id, /*device id*/ 1, |
| /*pointer id*/ 1, PointerEventType::TOUCH); |
| session->Enqueue(pointer.Up(2.5, 3.5)); |
| session->Enqueue(pointer.Remove(2.5, 3.5)); |
| } |
| RunLoopUntilIdle(); |
| |
| // Verify view's events. |
| { |
| const std::vector<InputEvent>& events = view.events(); |
| EXPECT_EQ(events.size(), 2u); |
| // UP |
| { |
| EXPECT_TRUE(events[0].is_pointer()); |
| const PointerEvent& up = events[0].pointer(); |
| EXPECT_EQ(up.x, 2.5); |
| EXPECT_EQ(up.y, 3.5); |
| } |
| |
| // REMOVE |
| { |
| EXPECT_TRUE(events[1].is_pointer()); |
| const PointerEvent& remove = events[1].pointer(); |
| EXPECT_EQ(remove.x, 2.5); |
| EXPECT_EQ(remove.y, 3.5); |
| } |
| } |
| |
| EXPECT_TRUE(listener.events().empty()) |
| << "Accessibility should stop receiving events in a stream after rejecting it."; |
| } |
| |
| // In this test three streams will be injected in the input system, where the first will be |
| // consumed, the second rejected and the third also consumed. |
| TEST_F(AccessibilityPointerEventsTest, AlternatingResponses) { |
| auto [root_view, view, compositor_id] = SetUpSingleView(k5x5x1); |
| |
| MockAccessibilityPointerEventListener listener(input_system()); |
| listener.SetResponses({ |
| {4, fuchsia::ui::input::accessibility::EventHandling::CONSUMED}, |
| {4, fuchsia::ui::input::accessibility::EventHandling::REJECTED}, |
| {4, fuchsia::ui::input::accessibility::EventHandling::CONSUMED}, |
| }); |
| |
| // Scene is now set up; send in the input. |
| { |
| scenic::Session* const session = root_view.session(); |
| PointerCommandGenerator pointer(compositor_id, /*device id*/ 1, |
| /*pointer id*/ 1, PointerEventType::TOUCH); |
| // A touch sequence that starts at the (2.5,2.5) location of the 5x5 display. |
| // First stream: |
| session->Enqueue(pointer.Add(1.5, 1.5)); |
| session->Enqueue(pointer.Down(1.5, 1.5)); |
| session->Enqueue(pointer.Up(1.5, 1.5)); |
| session->Enqueue(pointer.Remove(1.5, 1.5)); // Consume happens here. |
| // Second stream: |
| session->Enqueue(pointer.Add(2.5, 2.5)); |
| session->Enqueue(pointer.Down(2.5, 2.5)); |
| session->Enqueue(pointer.Up(2.5, 2.5)); |
| session->Enqueue(pointer.Remove(2.5, 2.5)); // Reject happens here. |
| // Third stream: |
| session->Enqueue(pointer.Add(3.5, 3.5)); |
| session->Enqueue(pointer.Down(3.5, 3.5)); |
| session->Enqueue(pointer.Up(3.5, 3.5)); |
| session->Enqueue(pointer.Remove(3.5, 3.5)); // Consume happens here. |
| } |
| RunLoopUntilIdle(); |
| |
| // Verify view's events. |
| // Here, only the focus event and events from the second stream should be present. |
| { |
| const std::vector<InputEvent>& events = view.events(); |
| EXPECT_EQ(events.size(), 5u); |
| // ADD |
| { |
| EXPECT_TRUE(events[0].is_pointer()); |
| const PointerEvent& add = events[0].pointer(); |
| EXPECT_EQ(add.x, 2.5); |
| EXPECT_EQ(add.y, 2.5); |
| } |
| |
| // FOCUS |
| EXPECT_TRUE(events[1].is_focus()); |
| |
| // DOWN |
| { |
| EXPECT_TRUE(events[2].is_pointer()); |
| const PointerEvent& down = events[2].pointer(); |
| EXPECT_EQ(down.x, 2.5); |
| EXPECT_EQ(down.y, 2.5); |
| } |
| |
| // UP |
| { |
| EXPECT_TRUE(events[3].is_pointer()); |
| const PointerEvent& up = events[3].pointer(); |
| EXPECT_EQ(up.x, 2.5); |
| EXPECT_EQ(up.y, 2.5); |
| } |
| |
| // REMOVE |
| { |
| EXPECT_TRUE(events[4].is_pointer()); |
| const PointerEvent& remove = events[4].pointer(); |
| EXPECT_EQ(remove.x, 2.5); |
| EXPECT_EQ(remove.y, 2.5); |
| } |
| } |
| |
| // Verify accessibility's events. |
| // The listener should see all events, as it is configured to see the entire stream before |
| // consuming / rejecting it. |
| { |
| const std::vector<AccessibilityPointerEvent>& events = listener.events(); |
| EXPECT_EQ(events.size(), 12u); |
| // ADD |
| { |
| const AccessibilityPointerEvent& add = events[0]; |
| EXPECT_EQ(add.phase(), Phase::ADD); |
| EXPECT_NEAR(add.ndc_point().x, -.4, kNdcEpsilon); |
| EXPECT_NEAR(add.ndc_point().y, -.4, kNdcEpsilon); |
| EXPECT_EQ(add.viewref_koid(), view.ViewKoid()); |
| EXPECT_EQ(add.local_point().x, 1.5); |
| EXPECT_EQ(add.local_point().y, 1.5); |
| } |
| |
| // DOWN |
| { |
| const AccessibilityPointerEvent& down = events[1]; |
| EXPECT_EQ(down.phase(), Phase::DOWN); |
| EXPECT_NEAR(down.ndc_point().x, -.4, kNdcEpsilon); |
| EXPECT_NEAR(down.ndc_point().y, -.4, kNdcEpsilon); |
| EXPECT_EQ(down.viewref_koid(), view.ViewKoid()); |
| EXPECT_EQ(down.local_point().x, 1.5); |
| EXPECT_EQ(down.local_point().y, 1.5); |
| } |
| |
| // UP |
| { |
| const AccessibilityPointerEvent& up = events[2]; |
| EXPECT_EQ(up.phase(), Phase::UP); |
| EXPECT_NEAR(up.ndc_point().x, -.4, kNdcEpsilon); |
| EXPECT_NEAR(up.ndc_point().y, -.4, kNdcEpsilon); |
| EXPECT_EQ(up.viewref_koid(), view.ViewKoid()); |
| EXPECT_EQ(up.local_point().x, 1.5); |
| EXPECT_EQ(up.local_point().y, 1.5); |
| } |
| |
| // REMOVE |
| { |
| const AccessibilityPointerEvent& remove = events[3]; |
| EXPECT_EQ(remove.phase(), Phase::REMOVE); |
| EXPECT_NEAR(remove.ndc_point().x, -.4, kNdcEpsilon); |
| EXPECT_NEAR(remove.ndc_point().y, -.4, kNdcEpsilon); |
| EXPECT_EQ(remove.viewref_koid(), view.ViewKoid()); |
| EXPECT_EQ(remove.local_point().x, 1.5); |
| EXPECT_EQ(remove.local_point().y, 1.5); |
| } |
| |
| // ADD |
| { |
| const AccessibilityPointerEvent& add = events[4]; |
| EXPECT_EQ(add.phase(), Phase::ADD); |
| EXPECT_EQ(add.ndc_point().x, 0); |
| EXPECT_EQ(add.ndc_point().y, 0); |
| EXPECT_EQ(add.viewref_koid(), view.ViewKoid()); |
| EXPECT_EQ(add.local_point().x, 2.5); |
| EXPECT_EQ(add.local_point().y, 2.5); |
| } |
| |
| // DOWN |
| { |
| const AccessibilityPointerEvent& down = events[5]; |
| EXPECT_EQ(down.phase(), Phase::DOWN); |
| EXPECT_EQ(down.ndc_point().x, 0); |
| EXPECT_EQ(down.ndc_point().y, 0); |
| EXPECT_EQ(down.viewref_koid(), view.ViewKoid()); |
| EXPECT_EQ(down.local_point().x, 2.5); |
| EXPECT_EQ(down.local_point().y, 2.5); |
| } |
| |
| // UP |
| { |
| const AccessibilityPointerEvent& up = events[6]; |
| EXPECT_EQ(up.phase(), Phase::UP); |
| EXPECT_EQ(up.ndc_point().x, 0); |
| EXPECT_EQ(up.ndc_point().y, 0); |
| EXPECT_EQ(up.viewref_koid(), view.ViewKoid()); |
| EXPECT_EQ(up.local_point().x, 2.5); |
| EXPECT_EQ(up.local_point().y, 2.5); |
| } |
| |
| // REMOVE |
| { |
| const AccessibilityPointerEvent& remove = events[7]; |
| EXPECT_EQ(remove.phase(), Phase::REMOVE); |
| EXPECT_EQ(remove.ndc_point().x, 0); |
| EXPECT_EQ(remove.ndc_point().y, 0); |
| EXPECT_EQ(remove.viewref_koid(), view.ViewKoid()); |
| EXPECT_EQ(remove.local_point().x, 2.5); |
| EXPECT_EQ(remove.local_point().y, 2.5); |
| } |
| |
| // ADD |
| { |
| const AccessibilityPointerEvent& add = events[8]; |
| EXPECT_EQ(add.phase(), Phase::ADD); |
| EXPECT_NEAR(add.ndc_point().x, .4, kNdcEpsilon); |
| EXPECT_NEAR(add.ndc_point().y, .4, kNdcEpsilon); |
| EXPECT_EQ(add.viewref_koid(), view.ViewKoid()); |
| EXPECT_EQ(add.local_point().x, 3.5); |
| EXPECT_EQ(add.local_point().y, 3.5); |
| } |
| |
| // DOWN |
| { |
| const AccessibilityPointerEvent& down = events[9]; |
| EXPECT_EQ(down.phase(), Phase::DOWN); |
| EXPECT_NEAR(down.ndc_point().x, .4, kNdcEpsilon); |
| EXPECT_NEAR(down.ndc_point().y, .4, kNdcEpsilon); |
| EXPECT_EQ(down.viewref_koid(), view.ViewKoid()); |
| EXPECT_EQ(down.local_point().x, 3.5); |
| EXPECT_EQ(down.local_point().y, 3.5); |
| } |
| |
| // UP |
| { |
| const AccessibilityPointerEvent& up = events[10]; |
| EXPECT_EQ(up.phase(), Phase::UP); |
| EXPECT_NEAR(up.ndc_point().x, .4, kNdcEpsilon); |
| EXPECT_NEAR(up.ndc_point().y, .4, kNdcEpsilon); |
| EXPECT_EQ(up.viewref_koid(), view.ViewKoid()); |
| EXPECT_EQ(up.local_point().x, 3.5); |
| EXPECT_EQ(up.local_point().y, 3.5); |
| } |
| |
| // REMOVE |
| { |
| const AccessibilityPointerEvent& remove = events[11]; |
| EXPECT_EQ(remove.phase(), Phase::REMOVE); |
| EXPECT_NEAR(remove.ndc_point().x, .4, kNdcEpsilon); |
| EXPECT_NEAR(remove.ndc_point().y, .4, kNdcEpsilon); |
| EXPECT_EQ(remove.viewref_koid(), view.ViewKoid()); |
| EXPECT_EQ(remove.local_point().x, 3.5); |
| EXPECT_EQ(remove.local_point().y, 3.5); |
| } |
| } |
| |
| // Make sure we didn't disconnect at some point for some reason. |
| EXPECT_TRUE(listener.is_registered()); |
| } |
| |
| // This test makes sure that if there is a stream in progress and the accessibility listener |
| // connects, the existing stream is not sent to the listener. |
| TEST_F(AccessibilityPointerEventsTest, DiscardActiveStreamOnConnection) { |
| auto [root_view, view, compositor_id] = SetUpSingleView(k5x5x1); |
| |
| // Scene is now set up, send in the input. |
| { |
| scenic::Session* const session = root_view.session(); |
| PointerCommandGenerator pointer(compositor_id, /*device id*/ 1, |
| /*pointer id*/ 1, PointerEventType::TOUCH); |
| // A touch sequence that starts at the (2.5,2.5) location of the 5x5 display. |
| session->Enqueue(pointer.Add(2.5, 2.5)); |
| session->Enqueue(pointer.Down(2.5, 2.5)); |
| } |
| RunLoopUntilIdle(); |
| |
| // Verify view's events. |
| EXPECT_EQ(view.events().size(), 3u); |
| |
| view.events().clear(); |
| |
| // Now, connect the accessibility listener in the middle of a stream. |
| MockAccessibilityPointerEventListener listener(input_system()); |
| |
| // Sends the rest of the stream. |
| { |
| scenic::Session* const session = root_view.session(); |
| PointerCommandGenerator pointer(compositor_id, /*device id*/ 1, |
| /*pointer id*/ 1, PointerEventType::TOUCH); |
| session->Enqueue(pointer.Up(2.5, 3.5)); |
| session->Enqueue(pointer.Remove(2.5, 3.5)); |
| } |
| RunLoopUntilIdle(); |
| |
| // Verify view's events. |
| { |
| const std::vector<InputEvent>& events = view.events(); |
| EXPECT_EQ(events.size(), 2u); |
| // UP |
| { |
| EXPECT_TRUE(events[0].is_pointer()); |
| const PointerEvent& up = events[0].pointer(); |
| EXPECT_EQ(up.x, 2.5); |
| EXPECT_EQ(up.y, 3.5); |
| } |
| |
| // REMOVE |
| { |
| EXPECT_TRUE(events[1].is_pointer()); |
| const PointerEvent& remove = events[1].pointer(); |
| EXPECT_EQ(remove.x, 2.5); |
| EXPECT_EQ(remove.y, 3.5); |
| } |
| } |
| |
| EXPECT_TRUE(listener.is_registered()); |
| EXPECT_TRUE(listener.events().empty()) << "Accessibility should not receive events from a stream " |
| "already in progress when it was registered."; |
| } |
| |
| // This tests makes sure that if there is an active stream, and accessibility disconnects, the |
| // stream is sent to regular clients. |
| TEST_F(AccessibilityPointerEventsTest, DispatchEventsAfterDisconnection) { |
| auto [root_view, view, compositor_id] = SetUpSingleView(k5x5x1); |
| |
| { |
| MockAccessibilityPointerEventListener listener(input_system()); |
| |
| // Scene is now set up; send in the input. |
| { |
| scenic::Session* const session = root_view.session(); |
| PointerCommandGenerator pointer(compositor_id, /*device id*/ 1, |
| /*pointer id*/ 1, PointerEventType::TOUCH); |
| // A touch sequence that starts at the (2.5,2.5) location of the 5x5 display. |
| session->Enqueue(pointer.Add(2.5, 2.5)); |
| session->Enqueue(pointer.Down(2.5, 2.5)); |
| } |
| RunLoopUntilIdle(); |
| |
| // Verify view's events. |
| EXPECT_TRUE(view.events().empty()); |
| |
| // Verify client's accessibility pointer events. Note that the listener must |
| // see two events here, as it will disconnect just after. |
| EXPECT_EQ(listener.events().size(), 2u); |
| |
| // Let the accessibility listener go out of scope without answering what we are going to do with |
| // the pointer events. |
| } |
| view.events().clear(); |
| |
| // Sends the rest of the stream. |
| { |
| scenic::Session* const session = root_view.session(); |
| PointerCommandGenerator pointer(compositor_id, /*device id*/ 1, |
| /*pointer id*/ 1, PointerEventType::TOUCH); |
| session->Enqueue(pointer.Up(2.5, 3.5)); |
| session->Enqueue(pointer.Remove(2.5, 3.5)); |
| } |
| RunLoopUntilIdle(); |
| |
| // Verify that all pointer events get routed to the view after disconnection. |
| { |
| const std::vector<InputEvent>& events = view.events(); |
| EXPECT_EQ(events.size(), 5u); |
| // ADD |
| { |
| EXPECT_TRUE(events[0].is_pointer()); |
| const PointerEvent& add = events[0].pointer(); |
| EXPECT_EQ(add.x, 2.5); |
| EXPECT_EQ(add.y, 2.5); |
| } |
| |
| // FOCUS |
| EXPECT_TRUE(events[1].is_focus()); |
| |
| // DOWN |
| { |
| EXPECT_TRUE(events[2].is_pointer()); |
| const PointerEvent& down = events[2].pointer(); |
| EXPECT_EQ(down.x, 2.5); |
| EXPECT_EQ(down.y, 2.5); |
| } |
| |
| // UP |
| { |
| EXPECT_TRUE(events[3].is_pointer()); |
| const PointerEvent& up = events[3].pointer(); |
| EXPECT_EQ(up.x, 2.5); |
| EXPECT_EQ(up.y, 3.5); |
| } |
| |
| // REMOVE |
| { |
| EXPECT_TRUE(events[4].is_pointer()); |
| const PointerEvent& remove = events[4].pointer(); |
| EXPECT_EQ(remove.x, 2.5); |
| EXPECT_EQ(remove.y, 3.5); |
| } |
| } |
| } |
| |
| // One pointer stream is injected in the input system. The listener rejects the pointer event after |
| // the ADD event. This test makes sure that the focus event gets sent, even though the stream is no |
| // longer buffered and its information is coming only from the active stream info data. |
| TEST_F(AccessibilityPointerEventsTest, FocusGetsSentAfterAddRejecting) { |
| auto [root_view, view, compositor_id] = SetUpSingleView(k5x5x1); |
| |
| MockAccessibilityPointerEventListener listener(input_system()); |
| listener.SetResponses({{1, fuchsia::ui::input::accessibility::EventHandling::REJECTED}}); |
| |
| // Scene is now set up; send in the input. |
| { |
| scenic::Session* const session = root_view.session(); |
| PointerCommandGenerator pointer(compositor_id, /*device id*/ 1, |
| /*pointer id*/ 1, PointerEventType::TOUCH); |
| // A touch sequence that starts at the (2.5,2.5) location of the 5x5 display. |
| session->Enqueue(pointer.Add(2.5, 2.5)); // Reject happens here. |
| session->Enqueue(pointer.Down(2.5, 2.5)); |
| } |
| RunLoopUntilIdle(); |
| |
| // Verify view's events. |
| { |
| const std::vector<InputEvent>& events = view.events(); |
| EXPECT_EQ(events.size(), 3u); |
| |
| // ADD |
| { |
| EXPECT_TRUE(events[0].is_pointer()); |
| const PointerEvent& add = events[0].pointer(); |
| EXPECT_EQ(add.x, 2.5); |
| EXPECT_EQ(add.y, 2.5); |
| } |
| |
| // FOCUS |
| EXPECT_TRUE(events[1].is_focus()); |
| |
| // DOWN |
| { |
| EXPECT_TRUE(events[2].is_pointer()); |
| const PointerEvent& down = events[2].pointer(); |
| EXPECT_EQ(down.x, 2.5); |
| EXPECT_EQ(down.y, 2.5); |
| } |
| } |
| |
| // Verify client's accessibility pointer events. |
| { |
| const std::vector<AccessibilityPointerEvent>& events = listener.events(); |
| EXPECT_EQ(events.size(), 2u); |
| // ADD |
| { |
| const AccessibilityPointerEvent& add = events[0]; |
| EXPECT_EQ(add.phase(), Phase::ADD); |
| EXPECT_EQ(add.ndc_point().x, 0); |
| EXPECT_EQ(add.ndc_point().y, 0); |
| EXPECT_EQ(add.viewref_koid(), view.ViewKoid()); |
| EXPECT_EQ(add.local_point().x, 2.5); |
| EXPECT_EQ(add.local_point().y, 2.5); |
| } |
| // TODO(rosswang): What's the second one? |
| } |
| |
| view.events().clear(); |
| listener.events().clear(); |
| |
| // Sends the rest of the stream. |
| { |
| scenic::Session* const session = root_view.session(); |
| PointerCommandGenerator pointer(compositor_id, /*device id*/ 1, |
| /*pointer id*/ 1, PointerEventType::TOUCH); |
| session->Enqueue(pointer.Up(2.5, 3.5)); |
| session->Enqueue(pointer.Remove(2.5, 3.5)); |
| } |
| RunLoopUntilIdle(); |
| |
| // Verify view's events. |
| { |
| const std::vector<InputEvent>& events = view.events(); |
| EXPECT_EQ(events.size(), 2u); |
| // UP |
| { |
| EXPECT_TRUE(events[0].is_pointer()); |
| const PointerEvent& up = events[0].pointer(); |
| EXPECT_EQ(up.x, 2.5); |
| EXPECT_EQ(up.y, 3.5); |
| } |
| |
| // REMOVE |
| { |
| EXPECT_TRUE(events[1].is_pointer()); |
| const PointerEvent& remove = events[1].pointer(); |
| EXPECT_EQ(remove.x, 2.5); |
| EXPECT_EQ(remove.y, 3.5); |
| } |
| } |
| |
| EXPECT_TRUE(listener.events().empty()); |
| } |
| |
| // In this test, there are two views. The root session injects a pointer event stream onto both. We |
| // alternate the elevation of the views; in each case, the topmost view's ViewRef KOID shold be |
| // observed. |
| TEST_F(AccessibilityPointerEventsTest, ExposeTopMostViewRefKoid) { |
| MockAccessibilityPointerEventListener listener(input_system()); |
| |
| auto [v_a, vh_a] = scenic::ViewTokenPair::New(); |
| auto [v_b, vh_b] = scenic::ViewTokenPair::New(); |
| |
| // Set up a scene with two views. |
| // Since we need to manipulate the scene graph, go ahead and do this all at function scope. |
| auto [root_view, root_resources] = CreateScene(); |
| scenic::Session* const session = root_view.session(); |
| scenic::Scene* const scene = &root_resources.scene; |
| |
| scenic::ViewHolder view_holder_a(session, std::move(vh_a), "View Holder A"), |
| view_holder_b(session, std::move(vh_b), "View Holder B"); |
| |
| view_holder_a.SetViewProperties(k5x5x1); |
| view_holder_b.SetViewProperties(k5x5x1); |
| |
| // Translate each view to control elevation. |
| view_holder_a.SetTranslation(0, 0, 1); |
| view_holder_b.SetTranslation(0, 0, 2); // B is lower than A. |
| |
| // Attach views to the scene. |
| scene->AddChild(view_holder_a); |
| scene->AddChild(view_holder_b); |
| |
| RequestToPresent(session); |
| |
| SessionWrapper view_a = CreateClient("a11y-view-a", std::move(v_a)), |
| view_b = CreateClient("a11y-view-b", std::move(v_b)); |
| |
| const uint32_t compositor_id = root_resources.compositor.id(); |
| |
| // Scene is now set up; send in the input. |
| { |
| PointerCommandGenerator pointer(compositor_id, /*device id*/ 1, |
| /*pointer id*/ 1, PointerEventType::TOUCH); |
| // A touch sequence that starts at the (2.5,2.5) location of the 5x5 display. |
| session->Enqueue(pointer.Add(2.5, 2.5)); |
| session->Enqueue(pointer.Down(2.5, 2.5)); |
| } |
| RunLoopUntilIdle(); |
| |
| // Verify views' events. |
| EXPECT_TRUE(view_a.events().empty()); |
| EXPECT_TRUE(view_b.events().empty()); |
| |
| // Verify accessibility's events. |
| { |
| const std::vector<AccessibilityPointerEvent>& events = listener.events(); |
| EXPECT_EQ(events.size(), 2u); |
| // ADD |
| { |
| const AccessibilityPointerEvent& add = events[0]; |
| EXPECT_EQ(add.phase(), Phase::ADD); |
| EXPECT_EQ(add.ndc_point().x, 0); |
| EXPECT_EQ(add.ndc_point().y, 0); |
| EXPECT_EQ(add.viewref_koid(), view_a.ViewKoid()); |
| EXPECT_EQ(add.local_point().x, 2.5); |
| EXPECT_EQ(add.local_point().y, 2.5); |
| } |
| |
| // DOWN |
| { |
| const AccessibilityPointerEvent& down = events[1]; |
| EXPECT_EQ(down.phase(), Phase::DOWN); |
| EXPECT_EQ(down.ndc_point().x, 0); |
| EXPECT_EQ(down.ndc_point().y, 0); |
| EXPECT_EQ(down.viewref_koid(), view_a.ViewKoid()); |
| EXPECT_EQ(down.local_point().x, 2.5); |
| EXPECT_EQ(down.local_point().y, 2.5); |
| } |
| } |
| |
| view_a.events().clear(); |
| view_b.events().clear(); |
| listener.events().clear(); |
| |
| // Raise B in elevation, higher than A. |
| view_holder_a.SetTranslation(0, 0, 2); |
| view_holder_b.SetTranslation(0, 0, 1); // B is higher than A. |
| RequestToPresent(session); |
| |
| // Scene is now set up, send in the input. |
| { |
| PointerCommandGenerator pointer(compositor_id, /*device id*/ 1, |
| /*pointer id*/ 1, PointerEventType::TOUCH); |
| // A touch sequence that ends at the (1.5,3.5) location of the 5x5 display. |
| session->Enqueue(pointer.Up(1.5, 3.5)); |
| session->Enqueue(pointer.Remove(1.5, 3.5)); |
| } |
| RunLoopUntilIdle(); |
| |
| // Verify views' events. |
| EXPECT_TRUE(view_a.events().empty()); |
| EXPECT_TRUE(view_b.events().empty()); |
| |
| // Verify accessibility's events. |
| { |
| const std::vector<AccessibilityPointerEvent>& events = listener.events(); |
| EXPECT_EQ(events.size(), 2u); |
| // UP |
| { |
| const AccessibilityPointerEvent& up = events[0]; |
| EXPECT_EQ(up.phase(), Phase::UP); |
| EXPECT_NEAR(up.ndc_point().x, -.4, kNdcEpsilon); |
| EXPECT_NEAR(up.ndc_point().y, .4, kNdcEpsilon); |
| EXPECT_EQ(up.viewref_koid(), view_b.ViewKoid()); |
| EXPECT_EQ(up.local_point().x, 1.5); |
| EXPECT_EQ(up.local_point().y, 3.5); |
| } |
| |
| // REMOVE |
| { |
| const AccessibilityPointerEvent& remove = events[1]; |
| EXPECT_EQ(remove.phase(), Phase::REMOVE); |
| EXPECT_NEAR(remove.ndc_point().x, -.4, kNdcEpsilon); |
| EXPECT_NEAR(remove.ndc_point().y, .4, kNdcEpsilon); |
| EXPECT_EQ(remove.viewref_koid(), view_b.ViewKoid()); |
| EXPECT_EQ(remove.local_point().x, 1.5); |
| EXPECT_EQ(remove.local_point().y, 3.5); |
| } |
| } |
| } |
| |
| // This test checks that semantic visibility works as intended. By setting the views to semantically |
| // invisble it should appear to accessibility as if they weren't hit, but other clients should still |
| // observe everything as normal. |
| TEST_F(AccessibilityPointerEventsTest, SemanticallyInvisible_ShouldNotBeSeenByA11y) { |
| MockAccessibilityPointerEventListener listener(input_system()); |
| // Immediately reject the stream. |
| listener.SetResponses({{1, fuchsia::ui::input::accessibility::EventHandling::REJECTED}}); |
| |
| auto [v_a, vh_a] = scenic::ViewTokenPair::New(); |
| auto [v_b, vh_b] = scenic::ViewTokenPair::New(); |
| |
| // Set up a scene with two views. |
| // Since we need to manipulate the scene graph, go ahead and do this all at function scope. |
| auto [root_view, root_resources] = CreateScene(); |
| scenic::Session* const session = root_view.session(); |
| scenic::Scene* const scene = &root_resources.scene; |
| |
| scenic::ViewHolder view_holder_a(session, std::move(vh_a), "View Holder A"), |
| view_holder_b(session, std::move(vh_b), "View Holder B"); |
| |
| view_holder_a.SetViewProperties(k5x5x1); |
| view_holder_b.SetViewProperties(k5x5x1); |
| |
| // Translate each view to control elevation. |
| view_holder_a.SetTranslation(0, 0, 1); |
| view_holder_b.SetTranslation(0, 0, 2); // B is lower than A. |
| |
| view_holder_a.SetSemanticVisibility(false); // A is semantically invisible. |
| |
| // Attach views to the scene. |
| scene->AddChild(view_holder_a); |
| scene->AddChild(view_holder_b); |
| |
| RequestToPresent(session); |
| |
| SessionWrapper view_a = CreateClient("a11y-view-a", std::move(v_a)), |
| view_b = CreateClient("a11y-view-b", std::move(v_b)); |
| |
| // Scene is now set up; send in the input. |
| { |
| { // Turn off parallel dispatch. |
| fuchsia::ui::input::SetParallelDispatchCmd parallel_dispatch_cmd; |
| parallel_dispatch_cmd.parallel_dispatch = false; |
| |
| InputCommand input_cmd; |
| input_cmd.set_set_parallel_dispatch(std::move(parallel_dispatch_cmd)); |
| session->Enqueue(std::move(input_cmd)); |
| } |
| |
| PointerCommandGenerator pointer(root_resources.compositor.id(), /*device id*/ 1, |
| /*pointer id*/ 1, PointerEventType::TOUCH); |
| // A touch sequence that starts at the (2.5,2.5) location of the 5x5 display. |
| session->Enqueue(pointer.Add(2.5, 2.5)); |
| session->Enqueue(pointer.Down(2.5, 2.5)); |
| } |
| RunLoopUntilIdle(); |
| |
| // Should look to A11y like B was the top hit. |
| EXPECT_FALSE(listener.events().empty()); |
| EXPECT_EQ(listener.events().front().viewref_koid(), utils::ExtractKoid(view_b.view_ref())); |
| |
| // Should look to the rest like A was the top hit. |
| EXPECT_TRUE(view_b.events().empty()); |
| |
| ASSERT_EQ(view_a.events().size(), 3u); // 3 since ADD gets translated to ADD + DOWN. |
| EXPECT_EQ(view_a.events()[0].pointer().phase, fuchsia::ui::input::PointerEventPhase::ADD); |
| EXPECT_TRUE(view_a.events()[1].is_focus()); |
| EXPECT_EQ(view_a.events()[2].pointer().phase, fuchsia::ui::input::PointerEventPhase::DOWN); |
| } |
| |
| // Create a larger 7x7 display, so that the scene (5x5) does not fully cover the display. |
| class LargeDisplayAccessibilityPointerEventsTest : public AccessibilityPointerEventsTest { |
| protected: |
| uint32_t test_display_width_px() const override { return 7; } |
| uint32_t test_display_height_px() const override { return 7; } |
| }; |
| |
| // This test has a DOWN event see an empty hit test, which means there is no client that latches. |
| // However, (1) accessibility should receive initial events, and (2) rejection by accessibility |
| // (on first MOVE) should trigger the expected focus change. |
| TEST_F(LargeDisplayAccessibilityPointerEventsTest, NoDownLatchAndA11yRejects) { |
| MockAccessibilityPointerEventListener listener(input_system()); |
| // Respond after three events: ADD / DOWN / MOVE. |
| listener.SetResponses({{3, fuchsia::ui::input::accessibility::EventHandling::REJECTED}}); |
| |
| auto [vt, vht] = scenic::ViewTokenPair::New(); |
| |
| // Set up a scene with one view. |
| auto [root_view, root_resources] = CreateScene(); |
| { |
| scenic::Session* const session = root_view.session(); |
| scenic::Scene* const scene = &root_resources.scene; |
| // Set scene origin (0, 0) to coincide with display (1, 1) -- this translation ensures that a |
| // 5x5 view is centered on the display. |
| scene->SetTranslation(1, 1, 0); |
| |
| scenic::ViewHolder view_holder(session, std::move(vht), "view holder"); |
| view_holder.SetViewProperties(k5x5x1); |
| scene->AddChild(view_holder); |
| |
| RequestToPresent(session); |
| } |
| |
| SessionWrapper view = CreateClient("a11y-single-view", std::move(vt)); |
| |
| // Transfer focus to view. |
| { |
| zx_koid_t scene_koid = focus_manager_.focus_chain()[0]; |
| auto status = focus_manager_.RequestFocus(scene_koid, view.ViewKoid()); |
| ASSERT_EQ(status, focus::FocusChangeStatus::kAccept); |
| } |
| RunLoopUntilIdle(); // Flush out focus events to clients. |
| |
| // Clear out events. |
| root_view.events().clear(); |
| view.events().clear(); |
| |
| // Setup is finished. Scene is now set up; send in the input. |
| { |
| PointerCommandGenerator pointer(root_resources.compositor.id(), /*device id*/ 1, |
| /*pointer id*/ 1, PointerEventType::TOUCH); |
| // A touch sequence that starts at the (0.5,0.5) location of the 7x7 display. |
| root_view.session()->Enqueue(pointer.Add(0.5, 0.5)); |
| root_view.session()->Enqueue(pointer.Down(0.5, 0.5)); |
| } |
| RunLoopUntilIdle(); |
| |
| // Verify view did not receive events. |
| EXPECT_EQ(view.events().size(), 0u); |
| // Verify root session did not receive events. |
| EXPECT_EQ(root_view.events().size(), 0u); |
| |
| // Send in third touch event, which causes accessibility to reject. |
| { |
| PointerCommandGenerator pointer(root_resources.compositor.id(), /*device id*/ 1, |
| /*pointer id*/ 1, PointerEventType::TOUCH); |
| // A touch sequence that starts at the (0.5,0.5) location of the 7x7 display. |
| root_view.session()->Enqueue(pointer.Move(0.5, 0.5)); |
| } |
| RunLoopUntilIdle(); |
| |
| // Verify view received only the unfocus event. |
| { |
| const std::vector<InputEvent>& events = view.events(); |
| ASSERT_EQ(events.size(), 1u); |
| EXPECT_TRUE(events[0].is_focus()); |
| EXPECT_FALSE(events[0].focus().focused); |
| } |
| |
| // Verify root session received only the focus event (since we revert to root of focus chain). |
| { |
| const std::vector<InputEvent>& events = root_view.events(); |
| ASSERT_EQ(events.size(), 1u); |
| EXPECT_TRUE(events[0].is_focus()); |
| EXPECT_TRUE(events[0].focus().focused); |
| } |
| } |
| |
| // This test has a DOWN event see an empty hit test, which means there is no client that latches. |
| // However, (1) accessibility should receive initial events, and (2) acceptance by accessibility (on |
| // first MOVE) means accessibility continues to observe events, despite absence of latch. |
| TEST_F(LargeDisplayAccessibilityPointerEventsTest, NoDownLatchAndA11yAccepts) { |
| MockAccessibilityPointerEventListener listener(input_system()); |
| // Respond after three events: ADD / DOWN / MOVE. |
| listener.SetResponses({{3, fuchsia::ui::input::accessibility::EventHandling::CONSUMED}}); |
| |
| auto [vt, vht] = scenic::ViewTokenPair::New(); |
| |
| // Set up a scene with one view. |
| auto [root_view, root_resources] = CreateScene(); |
| { |
| scenic::Session* const session = root_view.session(); |
| scenic::Scene* const scene = &root_resources.scene; |
| // Set scene origin (0, 0) to coincide with display (1, 1) -- this translation ensures that a |
| // 5x5 view is centered on the display. |
| scene->SetTranslation(1, 1, 0); |
| |
| scenic::ViewHolder view_holder(session, std::move(vht), "view holder"); |
| view_holder.SetViewProperties(k5x5x1); |
| scene->AddChild(view_holder); |
| |
| RequestToPresent(session); |
| } |
| |
| SessionWrapper view = CreateClient("a11y-single-view", std::move(vt)); |
| |
| // Transfer focus to view. |
| { |
| zx_koid_t scene_koid = focus_manager_.focus_chain()[0]; |
| auto status = focus_manager_.RequestFocus(scene_koid, view.ViewKoid()); |
| ASSERT_EQ(status, focus::FocusChangeStatus::kAccept); |
| } |
| RunLoopUntilIdle(); // Flush out focus events to clients. |
| |
| // Clear out events. |
| root_view.events().clear(); |
| view.events().clear(); |
| |
| // Setup is finished. Scene is now set up; send in the input. |
| { |
| PointerCommandGenerator pointer(root_resources.compositor.id(), /*device id*/ 1, |
| /*pointer id*/ 1, PointerEventType::TOUCH); |
| // A touch sequence that starts at the (0.5,0.5) location of the 7x7 display. |
| root_view.session()->Enqueue(pointer.Add(0.5, 0.5)); |
| root_view.session()->Enqueue(pointer.Down(0.5, 0.5)); |
| } |
| RunLoopUntilIdle(); |
| |
| // Verify view did not receive events. |
| EXPECT_EQ(view.events().size(), 0u); |
| // Verify root session did not receive events. |
| EXPECT_EQ(root_view.events().size(), 0u); |
| |
| // Send in third touch event, which causes accessibility to consume existing and future events. |
| { |
| PointerCommandGenerator pointer(root_resources.compositor.id(), /*device id*/ 1, |
| /*pointer id*/ 1, PointerEventType::TOUCH); |
| // Send MOVE events *over* the view. |
| root_view.session()->Enqueue(pointer.Move(1.5, 1.5)); |
| root_view.session()->Enqueue(pointer.Move(2.5, 2.5)); |
| } |
| RunLoopUntilIdle(); |
| |
| // Verify view did not receive events. |
| EXPECT_EQ(view.events().size(), 0u); |
| // Verify root session did not receive events. |
| EXPECT_EQ(root_view.events().size(), 0u); |
| |
| // Verify accessibility received 4 events so far. |
| { |
| const std::vector<AccessibilityPointerEvent>& events = listener.events(); |
| ASSERT_EQ(events.size(), 4u); |
| |
| // ADD |
| { |
| const AccessibilityPointerEvent& add = events[0]; |
| EXPECT_EQ(add.phase(), Phase::ADD); |
| EXPECT_EQ(add.viewref_koid(), ZX_KOID_INVALID); |
| } |
| // DOWN |
| { |
| const AccessibilityPointerEvent& down = events[1]; |
| EXPECT_EQ(down.phase(), Phase::DOWN); |
| EXPECT_EQ(down.viewref_koid(), ZX_KOID_INVALID); |
| } |
| // MOVE |
| { |
| const AccessibilityPointerEvent& move = events[2]; |
| EXPECT_EQ(move.phase(), Phase::MOVE); |
| EXPECT_EQ(move.viewref_koid(), view.ViewKoid()); |
| EXPECT_EQ(move.local_point().x, 0.5); |
| EXPECT_EQ(move.local_point().y, 0.5); |
| } |
| // MOVE |
| { |
| const AccessibilityPointerEvent& move = events[3]; |
| EXPECT_EQ(move.phase(), Phase::MOVE); |
| EXPECT_EQ(move.viewref_koid(), view.ViewKoid()); |
| EXPECT_EQ(move.local_point().x, 1.5); |
| EXPECT_EQ(move.local_point().y, 1.5); |
| } |
| } |
| } |
| |
| // Injection in TOP_HIT_AND_ANCESTORS_IN_TARGET mode should be delivered to a11y only if the context |
| // is the root view. This test registers an injector where the context is the root view. |
| TEST_F(AccessibilityPointerEventsTest, TopHitInjectionByRootView_IsDeliveredToA11y) { |
| auto [root_view, child_view, child_view2, compositor_id] = SetUpTwoViews(k5x5x1); |
| MockAccessibilityPointerEventListener listener(input_system()); |
| |
| // Scene is now set up; send in the input. |
| { |
| RegisterInjector(/*context=*/root_view.view_ref(), /*target=*/child_view2.view_ref(), |
| fuchsia::ui::pointerinjector::DispatchPolicy::TOP_HIT_AND_ANCESTORS_IN_TARGET, |
| fuchsia::ui::pointerinjector::DeviceType::TOUCH, |
| /*extents*/ {{/*min*/ {-100.f, -50.f}, /*max*/ {50.f, 100.f}}}); |
| Inject(2.5f, 2.5f, fuchsia::ui::pointerinjector::EventPhase::ADD); |
| Inject(2.5f, 2.5f, fuchsia::ui::pointerinjector::EventPhase::CHANGE); |
| Inject(2.5f, 2.5f, fuchsia::ui::pointerinjector::EventPhase::REMOVE); |
| RunLoopUntilIdle(); |
| } |
| |
| // Verify views have no events. |
| EXPECT_TRUE(child_view.events().empty()); |
| EXPECT_TRUE(child_view2.events().empty()); |
| |
| // Verify accessibility's events. |
| // 5 because ADD gets converted to ADD + DOWN and REMOVE to UP + REMOVE. |
| ASSERT_EQ(listener.events().size(), 5u); |
| EXPECT_EQ(listener.events()[0].phase(), fuchsia::ui::input::PointerEventPhase::ADD); |
| EXPECT_EQ(listener.events()[1].phase(), fuchsia::ui::input::PointerEventPhase::DOWN); |
| EXPECT_EQ(listener.events()[2].phase(), fuchsia::ui::input::PointerEventPhase::MOVE); |
| EXPECT_EQ(listener.events()[3].phase(), fuchsia::ui::input::PointerEventPhase::UP); |
| EXPECT_EQ(listener.events()[4].phase(), fuchsia::ui::input::PointerEventPhase::REMOVE); |
| } |
| |
| // Injection in TOP_HIT_AND_ANCESTORS_IN_TARGET mode should be delivered to a11y only if the context |
| // is the root view. This test registers an injector where the context is further down the graph. |
| TEST_F(AccessibilityPointerEventsTest, TopHitInjectionByNonRootView_IsNotDeliveredToA11y) { |
| auto [root_view, child_view, child_view2, compositor_id] = SetUpTwoViews(k5x5x1); |
| MockAccessibilityPointerEventListener listener(input_system()); |
| |
| // Scene is now set up; send in the input. |
| { |
| RegisterInjector(/*context=*/child_view.view_ref(), /*target=*/child_view2.view_ref(), |
| fuchsia::ui::pointerinjector::DispatchPolicy::TOP_HIT_AND_ANCESTORS_IN_TARGET, |
| fuchsia::ui::pointerinjector::DeviceType::TOUCH, |
| /*extents*/ {{/*min*/ {-100.f, -50.f}, /*max*/ {50.f, 100.f}}}); |
| Inject(2.5f, 2.5f, fuchsia::ui::pointerinjector::EventPhase::ADD); |
| Inject(2.5f, 2.5f, fuchsia::ui::pointerinjector::EventPhase::CHANGE); |
| Inject(2.5f, 2.5f, fuchsia::ui::pointerinjector::EventPhase::REMOVE); |
| RunLoopUntilIdle(); |
| } |
| |
| // Verify child_view2 received the events. |
| EXPECT_FALSE(child_view2.events().empty()); |
| |
| // Verify other view and accessibility have no events. |
| EXPECT_TRUE(child_view.events().empty()); |
| EXPECT_TRUE(listener.events().empty()); |
| } |
| |
| // Injection in EXCLUSIVE_TARGET mode should never be delivered to a11y. This test sets up |
| // a valid environment for a11y to get events, except for the DispatchPolicy. |
| TEST_F(AccessibilityPointerEventsTest, ExclusiveInjectionByRootView_IsNotDeliveredToA11y) { |
| auto [root_view, child_view, child_view2, compositor_id] = SetUpTwoViews(k5x5x1); |
| MockAccessibilityPointerEventListener listener(input_system()); |
| |
| // Scene is now set up; send in the input. |
| { |
| RegisterInjector(/*context=*/root_view.view_ref(), /*target=*/child_view2.view_ref(), |
| fuchsia::ui::pointerinjector::DispatchPolicy::EXCLUSIVE_TARGET, |
| fuchsia::ui::pointerinjector::DeviceType::TOUCH, |
| /*extents*/ {{/*min*/ {-100.f, -50.f}, /*max*/ {50.f, 100.f}}}); |
| Inject(2.5f, 2.5f, fuchsia::ui::pointerinjector::EventPhase::ADD); |
| Inject(2.5f, 2.5f, fuchsia::ui::pointerinjector::EventPhase::CHANGE); |
| Inject(2.5f, 2.5f, fuchsia::ui::pointerinjector::EventPhase::REMOVE); |
| RunLoopUntilIdle(); |
| } |
| |
| // Verify child_view2 received the events. |
| EXPECT_FALSE(child_view2.events().empty()); |
| |
| // Verify other view and accessibility have no events. |
| EXPECT_TRUE(child_view.events().empty()); |
| EXPECT_TRUE(listener.events().empty()); |
| } |
| |
| } // namespace |
| } // namespace lib_ui_input_tests |