| // 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 "src/ui/a11y/lib/screen_reader/screen_reader.h" |
| |
| #include <lib/gtest/test_loop_fixture.h> |
| #include <lib/sys/cpp/testing/component_context_provider.h> |
| #include <lib/syslog/cpp/macros.h> |
| |
| #include <gmock/gmock.h> |
| |
| #include "fuchsia/accessibility/gesture/cpp/fidl.h" |
| #include "src/ui/a11y/bin/a11y_manager/tests/util/util.h" |
| #include "src/ui/a11y/lib/annotation/tests/mocks/mock_annotation_view.h" |
| #include "src/ui/a11y/lib/focus_chain/tests/mocks/mock_focus_chain_registry.h" |
| #include "src/ui/a11y/lib/focus_chain/tests/mocks/mock_focus_chain_requester.h" |
| #include "src/ui/a11y/lib/gesture_manager/gesture_listener_registry.h" |
| #include "src/ui/a11y/lib/gesture_manager/gesture_manager.h" |
| #include "src/ui/a11y/lib/gesture_manager/recognizers/one_finger_drag_recognizer.h" |
| #include "src/ui/a11y/lib/gesture_manager/recognizers/one_finger_n_tap_recognizer.h" |
| #include "src/ui/a11y/lib/gesture_manager/recognizers/swipe_recognizer_base.h" |
| #include "src/ui/a11y/lib/gesture_manager/tests/mocks/mock_gesture_handler.h" |
| #include "src/ui/a11y/lib/gesture_manager/tests/mocks/mock_gesture_listener.h" |
| #include "src/ui/a11y/lib/screen_reader/focus/tests/mocks/mock_a11y_focus_manager.h" |
| #include "src/ui/a11y/lib/screen_reader/tests/mocks/mock_screen_reader_context.h" |
| #include "src/ui/a11y/lib/screen_reader/tests/mocks/mock_tts_engine.h" |
| #include "src/ui/a11y/lib/semantics/tests/mocks/mock_semantic_provider.h" |
| #include "src/ui/a11y/lib/semantics/tests/mocks/mock_semantic_tree.h" |
| #include "src/ui/a11y/lib/semantics/tests/mocks/mock_semantic_tree_service_factory.h" |
| #include "src/ui/a11y/lib/semantics/tests/mocks/mock_semantics_event_manager.h" |
| #include "src/ui/a11y/lib/testing/input.h" |
| #include "src/ui/a11y/lib/tts/tests/mocks/mock_tts_manager.h" |
| #include "src/ui/a11y/lib/tts/tts_manager.h" |
| #include "src/ui/a11y/lib/util/util.h" |
| #include "src/ui/a11y/lib/view/tests/mocks/mock_view_semantics.h" |
| #include "src/ui/a11y/lib/view/view_manager.h" |
| |
| namespace accessibility_test { |
| namespace { |
| |
| using AccessibilityPointerEvent = fuchsia::ui::input::accessibility::PointerEvent; |
| using GestureType = a11y::GestureHandler::GestureType; |
| using PointerEventPhase = fuchsia::ui::input::PointerEventPhase; |
| using fuchsia::accessibility::gesture::Type; |
| using fuchsia::accessibility::semantics::Node; |
| using Phase = fuchsia::ui::input::PointerEventPhase; |
| using testing::ElementsAre; |
| using testing::StrEq; |
| |
| class MockScreenReaderActionRegistryImpl : public a11y::ScreenReaderActionRegistry, |
| public a11y::ScreenReaderAction { |
| public: |
| MockScreenReaderActionRegistryImpl() = default; |
| ~MockScreenReaderActionRegistryImpl() override = default; |
| void AddAction(std::string name, std::unique_ptr<ScreenReaderAction> action) override { |
| actions_.insert(std::move(name)); |
| } |
| |
| ScreenReaderAction* GetActionByName(const std::string& name) override { |
| auto action_it = actions_.find(name); |
| if (action_it == actions_.end()) { |
| return nullptr; |
| } |
| invoked_actions_.push_back(name); |
| return this; |
| } |
| |
| void Run(ActionData process_data) override {} |
| |
| std::vector<std::string>& invoked_actions() { return invoked_actions_; } |
| |
| private: |
| std::unordered_set<std::string> actions_; |
| std::vector<std::string> invoked_actions_; |
| }; |
| |
| class ScreenReaderTest : public gtest::TestLoopFixture { |
| public: |
| ScreenReaderTest() = default; |
| ~ScreenReaderTest() override = default; |
| |
| void InitializeScreenReader() { |
| screen_reader_ = std::make_unique<a11y::ScreenReader>( |
| std::move(context_), view_manager_.get(), gesture_listener_registry_.get(), |
| mock_tts_manager_.get(), announce_screen_reader_enabled_, std::move(mock_action_registry_)); |
| screen_reader_->BindGestures(mock_gesture_handler_.get()); |
| gesture_listener_registry_->Register(mock_gesture_listener_->NewBinding(), []() {}); |
| |
| semantic_provider_->SetSemanticsEnabled(true); |
| view_manager_->SetSemanticsEnabled(true); |
| factory_ptr_->service()->EnableSemanticsUpdates(true); |
| } |
| |
| void SetUp() override { |
| gtest::TestLoopFixture::SetUp(); |
| |
| factory_ = std::make_unique<MockSemanticTreeServiceFactory>(); |
| factory_ptr_ = factory_.get(), |
| context_provider_ = std::make_unique<sys::testing::ComponentContextProvider>(); |
| mock_gesture_handler_ = std::make_unique<MockGestureHandler>(); |
| view_manager_ = std::make_unique<a11y::ViewManager>( |
| std::move(factory_), std::make_unique<MockViewSemanticsFactory>(), |
| std::make_unique<MockAnnotationViewFactory>(), |
| std::make_unique<MockSemanticsEventManager>(), context_provider_->context(), |
| context_provider_->context()->outgoing()->debug_dir()); |
| context_ = std::make_unique<MockScreenReaderContext>(); |
| context_ptr_ = context_.get(); |
| a11y_focus_manager_ptr_ = context_ptr_->mock_a11y_focus_manager_ptr(); |
| mock_speaker_ptr_ = context_ptr_->mock_speaker_ptr(); |
| mock_action_registry_ = std::make_unique<MockScreenReaderActionRegistryImpl>(); |
| mock_action_registry_ptr_ = mock_action_registry_.get(); |
| mock_tts_manager_ = std::make_unique<MockTtsManager>(context_provider_->context()); |
| semantic_provider_ = std::make_unique<MockSemanticProvider>(view_manager_.get()); |
| gesture_manager_ = std::make_unique<a11y::GestureManager>(); |
| gesture_listener_registry_ = std::make_unique<a11y::GestureListenerRegistry>(); |
| mock_gesture_handler_ = std::make_unique<MockGestureHandler>(); |
| mock_gesture_listener_ = std::make_unique<MockGestureListener>(); |
| } |
| |
| void ConnectSpeakerAndEngine() { |
| // The speaker and engine need to be connected to the tts manager before the |
| // screen reader announces it's on. In order to verify that the screen reader |
| // correctly vocalizes, we need to expicitly connect the speaker and engine. |
| fuchsia::accessibility::tts::EnginePtr engine_ptr; |
| mock_tts_manager_->OpenEngine( |
| engine_ptr.NewRequest(), |
| [](fuchsia::accessibility::tts::TtsManager_OpenEngine_Result result) {}); |
| RunLoopUntilIdle(); |
| |
| MockTtsEngine mock_tts_engine; |
| mock_tts_manager_->RegisterEngine( |
| mock_tts_engine.GetHandle(), |
| [](fuchsia::accessibility::tts::EngineRegistry_RegisterEngine_Result result) {}); |
| RunLoopUntilIdle(); |
| } |
| |
| bool announce_screen_reader_enabled_ = true; |
| std::unique_ptr<MockSemanticTreeServiceFactory> factory_; |
| MockSemanticTreeServiceFactory* factory_ptr_; |
| std::unique_ptr<sys::testing::ComponentContextProvider> context_provider_; |
| std::unique_ptr<a11y::ViewManager> view_manager_; |
| std::unique_ptr<a11y::GestureManager> gesture_manager_; |
| std::unique_ptr<a11y::GestureListenerRegistry> gesture_listener_registry_; |
| std::unique_ptr<MockGestureListener> mock_gesture_listener_; |
| std::unique_ptr<MockGestureHandler> mock_gesture_handler_; |
| |
| std::unique_ptr<MockScreenReaderContext> context_; |
| MockScreenReaderContext* context_ptr_; |
| MockA11yFocusManager* a11y_focus_manager_ptr_; |
| MockScreenReaderContext::MockSpeaker* mock_speaker_ptr_; |
| std::unique_ptr<MockScreenReaderActionRegistryImpl> mock_action_registry_; |
| MockScreenReaderActionRegistryImpl* mock_action_registry_ptr_; |
| std::unique_ptr<MockTtsManager> mock_tts_manager_; |
| std::unique_ptr<a11y::ScreenReader> screen_reader_; |
| std::unique_ptr<MockSemanticProvider> semantic_provider_; |
| }; // namespace |
| |
| TEST_F(ScreenReaderTest, GestureHandlersAreRegisteredIntheRightOrder) { |
| InitializeScreenReader(); |
| // The order in which the Screen Reader registers the gesture handlers at startup is relevant. |
| // Each registered handler is saved in the mock, so we can check if they are in the right order |
| // here. |
| EXPECT_THAT(mock_gesture_handler_->bound_gestures(), |
| ElementsAre(GestureType::kThreeFingerUpSwipe, GestureType::kThreeFingerDownSwipe, |
| GestureType::kThreeFingerLeftSwipe, GestureType::kThreeFingerRightSwipe, |
| GestureType::kOneFingerDownSwipe, GestureType::kOneFingerUpSwipe, |
| GestureType::kOneFingerLeftSwipe, GestureType::kOneFingerRightSwipe, |
| GestureType::kOneFingerDoubleTap, GestureType::kOneFingerSingleTap, |
| GestureType::kOneFingerDrag, GestureType::kTwoFingerSingleTap)); |
| } |
| |
| TEST_F(ScreenReaderTest, RegisteredActionsAreInvokedWhenGestureTriggers) { |
| InitializeScreenReader(); |
| mock_gesture_handler_->TriggerGesture( |
| GestureType::kThreeFingerUpSwipe); // corresponds to physical right. |
| mock_gesture_handler_->TriggerGesture( |
| GestureType::kThreeFingerDownSwipe); // Corresponds to physical left. |
| mock_gesture_handler_->TriggerGesture( |
| GestureType::kThreeFingerLeftSwipe); // Corresponds to physical up |
| mock_gesture_handler_->TriggerGesture( |
| GestureType::kThreeFingerRightSwipe); // Corresponds to physical down. |
| mock_gesture_handler_->TriggerGesture( |
| GestureType::kOneFingerUpSwipe); // Corresponds to physical right. |
| mock_gesture_handler_->TriggerGesture( |
| GestureType::kOneFingerDownSwipe); // Corresponds to physical left. |
| mock_gesture_handler_->TriggerGesture( |
| GestureType::kOneFingerLeftSwipe); // Corresponds to a physical up. |
| mock_gesture_handler_->TriggerGesture( |
| GestureType::kOneFingerRightSwipe); // Corresponds to a physical down. |
| mock_gesture_handler_->TriggerGesture(GestureType::kOneFingerDoubleTap); |
| // Note that since one finger single tap and drag both trigger the explore action, we expect to |
| // see it twice in the list of called actions. |
| mock_gesture_handler_->TriggerGesture(GestureType::kOneFingerSingleTap); |
| mock_gesture_handler_->TriggerGesture(GestureType::kOneFingerDrag); |
| RunLoopUntilIdle(); |
| EXPECT_THAT( |
| mock_action_registry_ptr_->invoked_actions(), |
| ElementsAre(StrEq("Three finger Right Swipe Action"), StrEq("Three finger Left Swipe Action"), |
| StrEq("Three finger Up Swipe Action"), StrEq("Three finger Down Swipe Action"), |
| StrEq("Next Action"), StrEq("Previous Action"), |
| StrEq("Previous Semantic Level Action"), StrEq("Next Semantic Level Action"), |
| StrEq("Default Action"), StrEq("Explore Action"), StrEq("Explore Action"))); |
| } |
| |
| TEST_F(ScreenReaderTest, TrivialActionsAreInvokedWhenGestureTriggers) { |
| InitializeScreenReader(); |
| // Trivial actions are not registered in the action registry, but are jusst the callback parked at |
| // the gesture handler. Verify that the results of the callback are seen when it runs. |
| mock_gesture_handler_->TriggerGesture(GestureType::kTwoFingerSingleTap); |
| EXPECT_TRUE(mock_speaker_ptr_->ReceivedCancel()); |
| } |
| |
| TEST_F(ScreenReaderTest, ScreenReaderSpeaksWhenItTurnsOnAndOff) { |
| InitializeScreenReader(); |
| // No output should be spoken until the tts engine is connected. |
| EXPECT_TRUE(mock_speaker_ptr_->message_ids().empty()); |
| |
| // The screen reader will not announce it's on until the speaker and engine |
| // are connected. |
| ConnectSpeakerAndEngine(); |
| |
| // The screen reader object has already been initialized, check if it announced it: |
| EXPECT_EQ(mock_speaker_ptr_->message_ids().size(), 1u); |
| EXPECT_EQ(mock_speaker_ptr_->message_ids()[0], |
| fuchsia::intl::l10n::MessageIds ::SCREEN_READER_ON_HINT); |
| // Because the Screen Reader owns the speaker, when it is destroyed, so is the speaker. |
| // This callback makes sure that we have the chance to take a last look at the speaker before it |
| // goes out of scope. |
| bool callback_ran = false; |
| MockScreenReaderContext::MockSpeaker::OnDestructionCallback callback = |
| [&callback_ran](MockScreenReaderContext::MockSpeaker* speaker) { |
| callback_ran = true; |
| EXPECT_EQ(speaker->epitaph(), fuchsia::intl::l10n::MessageIds ::SCREEN_READER_OFF_HINT); |
| }; |
| mock_speaker_ptr_->set_on_destruction_callback(std::move(callback)); |
| screen_reader_.reset(); |
| EXPECT_TRUE(callback_ran); |
| } |
| |
| TEST_F(ScreenReaderTest, ScreenReaderSpeaksWhenInitializedAfterEngineAndSpeakerConnected) { |
| // The screen reader will not announce it's on until the speaker and engine |
| // are connected. |
| ConnectSpeakerAndEngine(); |
| |
| // When the screen reader's destructor was called above |
| // (via sceen_reader_.reset()), screen_reader_'s callback should have been |
| // unregistered from the tts manager. If not, the tts manager would have |
| // invoekd the stale callback. This check ensures that the unregistration was |
| // handled correctly. |
| EXPECT_TRUE(mock_speaker_ptr_->message_ids().empty()); |
| |
| InitializeScreenReader(); |
| |
| EXPECT_EQ(mock_speaker_ptr_->message_ids().size(), 1u); |
| } |
| |
| TEST_F(ScreenReaderTest, NextOrPreviousActionInvokesActionsBasedOnTheSemanticLevel) { |
| InitializeScreenReader(); |
| |
| // This test makes sure that when the next / previous action is invoked, bound to right / left one |
| // finger swipes, it corresponds to the appropriate action to the current semantic level. |
| EXPECT_EQ(context_ptr_->semantic_level(), |
| a11y::ScreenReaderContext::SemanticLevel::kNormalNavigation); |
| mock_gesture_handler_->TriggerGesture( |
| GestureType::kOneFingerUpSwipe); // Corresponds to physical right. |
| mock_gesture_handler_->TriggerGesture( |
| GestureType::kOneFingerDownSwipe); // Corresponds to physical left. |
| context_ptr_->set_semantic_level(a11y::ScreenReaderContext::SemanticLevel::kAdjustValue); |
| mock_gesture_handler_->TriggerGesture( |
| GestureType::kOneFingerUpSwipe); // Corresponds to physical right. |
| mock_gesture_handler_->TriggerGesture( |
| GestureType::kOneFingerDownSwipe); // Corresponds to physical left. |
| EXPECT_THAT( |
| mock_action_registry_ptr_->invoked_actions(), |
| ElementsAre(StrEq("Next Action"), StrEq("Previous Action"), |
| StrEq("Increment Range Value Action"), StrEq("Decrement Range Value Action"))); |
| } |
| |
| TEST_F(ScreenReaderTest, SemanticEventsTriggerScreenReaderAction) { |
| InitializeScreenReader(); |
| |
| view_manager_->GetSemanticsEventManager()->Register( |
| screen_reader_->GetSemanticsEventListenerWeakPtr()); |
| view_manager_->GetSemanticsEventManager()->OnEvent( |
| {.event_type = a11y::SemanticsEventType::kSemanticTreeUpdated}); |
| EXPECT_THAT(mock_action_registry_ptr_->invoked_actions(), |
| ElementsAre(StrEq("Recover A11Y Focus Action"))); |
| } |
| |
| TEST_F(ScreenReaderTest, ScreenReaderSilentWhenSpecifiedDuringInit) { |
| announce_screen_reader_enabled_ = false; |
| InitializeScreenReader(); |
| |
| // No output should be spoken until the tts engine is connected. |
| EXPECT_TRUE(mock_speaker_ptr_->message_ids().empty()); |
| |
| ConnectSpeakerAndEngine(); |
| |
| // No output should be spoken since reboot was not user-initiated. |
| EXPECT_TRUE(mock_speaker_ptr_->message_ids().empty()); |
| } |
| |
| } // namespace |
| } // namespace accessibility_test |