| // Copyright 2020 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. |
| #![cfg(test)] |
| |
| use { |
| anyhow::{Context as _, Result}, |
| assert_matches::assert_matches, |
| fidl::endpoints::DiscoverableProtocolMarker, |
| fidl::endpoints::{create_request_stream, ProtocolMarker as _}, |
| fidl_fuchsia_input as input, fidl_fuchsia_ui_input as fidl_input, |
| fidl_fuchsia_ui_input3 as ui_input3, fidl_fuchsia_ui_keyboard_focus as fidl_focus, |
| fidl_fuchsia_ui_views as ui_views, fuchsia_async as fasync, |
| fuchsia_component_test::{ |
| Capability, ChildOptions, ChildRef, RealmBuilder, RealmInstance, Ref, Route, |
| }, |
| fuchsia_scenic as scenic, fuchsia_zircon as zx, |
| futures::FutureExt, |
| futures::{ |
| future, |
| stream::{FusedStream, StreamExt}, |
| }, |
| test_case::test_case, |
| test_helpers::create_key_event, |
| }; |
| |
| const URL_TEXT_MANAGER: &str = |
| "fuchsia-pkg://fuchsia.com/keyboard_test_parallel#meta/text_manager.cm"; |
| |
| // A shorthand for type safe routing the given protocol from a child to the parent. |
| async fn route_to_parent<P: DiscoverableProtocolMarker>( |
| builder: &RealmBuilder, |
| child: &ChildRef, |
| ) -> Result<(), fuchsia_component_test::error::Error> { |
| builder |
| .add_route( |
| Route::new().capability(Capability::protocol::<P>()).from(child).to(Ref::parent()), |
| ) |
| .await |
| } |
| |
| /// Wrapper for a `NestedEnvironment` that exposes the protocols offered by text_manager. |
| /// Running each test method in its own environment allows the tests to be run in parallel. |
| struct TestEnvironment { |
| realm: RealmInstance, |
| } |
| |
| impl TestEnvironment { |
| async fn new() -> Result<Self> { |
| let builder = RealmBuilder::new().await?; |
| let text_manager = |
| builder.add_child("text_manager", URL_TEXT_MANAGER, ChildOptions::new()).await?; |
| |
| route_to_parent::<ui_input3::KeyboardMarker>(&builder, &text_manager).await?; |
| route_to_parent::<fidl_input::ImeServiceMarker>(&builder, &text_manager).await?; |
| route_to_parent::<ui_input3::KeyEventInjectorMarker>(&builder, &text_manager).await?; |
| route_to_parent::<fidl_focus::ControllerMarker>(&builder, &text_manager).await?; |
| builder |
| .add_route( |
| Route::new() |
| .capability(Capability::protocol::<fidl_fuchsia_logger::LogSinkMarker>()) |
| .from(Ref::parent()) |
| .to(&text_manager), |
| ) |
| .await?; |
| |
| let realm = builder.build().await?; |
| |
| Ok(TestEnvironment { realm }) |
| } |
| |
| /// Connects to the given discoverable service in the `NestedEnvironment`, with a readable |
| /// context on error. |
| fn connect_to_env_service<P>(&self) -> Result<P> |
| where |
| P: fidl::endpoints::Proxy, |
| P::Protocol: fidl::endpoints::DiscoverableProtocolMarker, |
| { |
| let conn = |
| self.realm.root.connect_to_protocol_at_exposed_dir::<P::Protocol>().with_context( |
| || format!("Failed to connect to test realm's {}", P::Protocol::DEBUG_NAME), |
| ); |
| tracing::debug!("Connected to test realm's {}", P::Protocol::DEBUG_NAME); |
| conn |
| } |
| |
| fn connect_to_focus_controller(&self) -> Result<fidl_focus::ControllerProxy> { |
| self.connect_to_env_service::<_>() |
| } |
| |
| fn connect_to_keyboard_service(&self) -> Result<ui_input3::KeyboardProxy> { |
| self.connect_to_env_service::<_>() |
| } |
| |
| fn connect_to_key_event_injector(&self) -> Result<ui_input3::KeyEventInjectorProxy> { |
| self.connect_to_env_service::<_>() |
| } |
| } |
| |
| fn create_key_down_event(key: input::Key, modifiers: ui_input3::Modifiers) -> ui_input3::KeyEvent { |
| ui_input3::KeyEvent { |
| timestamp: Some(0), |
| key: Some(key), |
| modifiers: Some(modifiers), |
| type_: Some(ui_input3::KeyEventType::Pressed), |
| ..Default::default() |
| } |
| } |
| |
| fn create_key_up_event(key: input::Key, modifiers: ui_input3::Modifiers) -> ui_input3::KeyEvent { |
| ui_input3::KeyEvent { |
| timestamp: Some(0), |
| key: Some(key), |
| modifiers: Some(modifiers), |
| type_: Some(ui_input3::KeyEventType::Released), |
| ..Default::default() |
| } |
| } |
| |
| async fn expect_key_event( |
| listener: &mut ui_input3::KeyboardListenerRequestStream, |
| ) -> ui_input3::KeyEvent { |
| let listener_request = listener.next().await; |
| if let Some(Ok(ui_input3::KeyboardListenerRequest::OnKeyEvent { event, responder, .. })) = |
| listener_request |
| { |
| responder.send(ui_input3::KeyEventStatus::Handled).expect("responding from key listener"); |
| event |
| } else { |
| panic!("Expected key event, got {:?}", listener_request); |
| } |
| } |
| |
| async fn dispatch_and_expect_key_event<'a>( |
| key_dispatcher: &'a test_helpers::KeySimulator<'a>, |
| listener: &mut ui_input3::KeyboardListenerRequestStream, |
| event: ui_input3::KeyEvent, |
| ) -> Result<()> { |
| let (was_handled, event_result) = |
| future::join(key_dispatcher.dispatch(event.clone()), expect_key_event(listener)).await; |
| |
| assert_eq!(was_handled?, true); |
| assert_eq!(event_result.key, event.key); |
| assert_eq!(event_result.type_, event.type_); |
| Ok(()) |
| } |
| |
| async fn expect_key_and_modifiers( |
| listener: &mut ui_input3::KeyboardListenerRequestStream, |
| key: input::Key, |
| modifiers: ui_input3::Modifiers, |
| ) { |
| let event = expect_key_event(listener).await; |
| assert_eq!(event.key, Some(key)); |
| assert_eq!(event.modifiers, Some(modifiers)); |
| } |
| |
| async fn test_disconnecting_keyboard_client_disconnects_listener_with_connections( |
| focus_ctl: fidl_focus::ControllerProxy, |
| key_simulator: &'_ test_helpers::KeySimulator<'_>, |
| keyboard_service_client: ui_input3::KeyboardProxy, |
| keyboard_service_other_client: &ui_input3::KeyboardProxy, |
| ) -> Result<()> { |
| tracing::debug!("test_disconnecting_keyboard_client_disconnects_listener_with_connections"); |
| |
| // Create fake client. |
| let (listener_client_end, mut listener) = |
| fidl::endpoints::create_request_stream::<ui_input3::KeyboardListenerMarker>()?; |
| let view_ref = scenic::ViewRefPair::new()?.view_ref; |
| |
| keyboard_service_client |
| .add_listener(scenic::duplicate_view_ref(&view_ref)?, listener_client_end) |
| .await |
| .expect("add_listener for first client"); |
| |
| // Create another fake client. |
| let (other_listener_client_end, mut other_listener) = |
| fidl::endpoints::create_request_stream::<ui_input3::KeyboardListenerMarker>()?; |
| let other_view_ref = scenic::ViewRefPair::new()?.view_ref; |
| |
| keyboard_service_other_client |
| .add_listener(scenic::duplicate_view_ref(&other_view_ref)?, other_listener_client_end) |
| .await |
| .expect("add_listener for another client"); |
| |
| // Focus second client. |
| focus_ctl.notify(scenic::duplicate_view_ref(&other_view_ref)?).await?; |
| |
| // Drop proxy, emulating first client disconnecting from it. |
| std::mem::drop(keyboard_service_client); |
| |
| // Expect disconnected client key event listener to be disconnected as well. |
| assert_matches!(listener.next().await, None); |
| assert_matches!(listener.is_terminated(), true); |
| |
| // Ensure that the other client is still connected. |
| let (key, modifiers) = (input::Key::A, ui_input3::Modifiers::CAPS_LOCK); |
| let dispatched_event = create_key_down_event(key, modifiers); |
| |
| let (was_handled, _) = future::join( |
| key_simulator.dispatch(dispatched_event), |
| expect_key_and_modifiers(&mut other_listener, key, modifiers), |
| ) |
| .await; |
| |
| assert_eq!(was_handled?, true); |
| |
| let dispatched_event = create_key_up_event(key, modifiers); |
| let (was_handled, _) = future::join( |
| key_simulator.dispatch(dispatched_event), |
| expect_key_and_modifiers(&mut other_listener, key, modifiers), |
| ) |
| .await; |
| |
| assert_eq!(was_handled?, true); |
| Ok(()) |
| } |
| |
| #[fuchsia::test(logging_tags = ["keyboard3_integration_test"])] |
| async fn test_disconnecting_keyboard_client_disconnects_listener_via_key_event_injector( |
| ) -> Result<()> { |
| let test_env = TestEnvironment::new().await?; |
| |
| let key_event_injector = test_env.connect_to_key_event_injector()?; |
| |
| let key_dispatcher = |
| test_helpers::KeyEventInjectorDispatcher { key_event_injector: &key_event_injector }; |
| let key_simulator = test_helpers::KeySimulator::new(&key_dispatcher); |
| |
| let keyboard_service_client_a = test_env.connect_to_keyboard_service().context("client_a")?; |
| |
| let keyboard_service_client_b = test_env.connect_to_keyboard_service().context("client_b")?; |
| |
| test_disconnecting_keyboard_client_disconnects_listener_with_connections( |
| test_env.connect_to_focus_controller()?, |
| &key_simulator, |
| // This one will be dropped as part of the test, so needs to be moved. |
| keyboard_service_client_a, |
| &keyboard_service_client_b, |
| ) |
| .await?; |
| |
| Ok(()) |
| } |
| |
| async fn test_sync_cancel_with_connections( |
| focus_ctl: fidl_focus::ControllerProxy, |
| key_simulator: &'_ test_helpers::KeySimulator<'_>, |
| keyboard_service_client_a: &ui_input3::KeyboardProxy, |
| keyboard_service_client_b: &ui_input3::KeyboardProxy, |
| ) -> Result<()> { |
| // Create fake client. |
| let (listener_client_end_a, mut listener_a) = |
| fidl::endpoints::create_request_stream::<ui_input3::KeyboardListenerMarker>()?; |
| let view_ref_a = scenic::ViewRefPair::new()?.view_ref; |
| |
| keyboard_service_client_a |
| .add_listener(scenic::duplicate_view_ref(&view_ref_a)?, listener_client_end_a) |
| .await |
| .expect("add_listener for first client"); |
| |
| // Create another fake client. |
| let (listener_client_end_b, mut listener_b) = |
| fidl::endpoints::create_request_stream::<ui_input3::KeyboardListenerMarker>()?; |
| let view_ref_b = scenic::ViewRefPair::new()?.view_ref; |
| |
| keyboard_service_client_b |
| .add_listener(scenic::duplicate_view_ref(&view_ref_b)?, listener_client_end_b) |
| .await |
| .expect("add_listener for another client"); |
| |
| let key1 = input::Key::A; |
| let event1_press = ui_input3::KeyEvent { |
| timestamp: Some(0), |
| key: Some(key1), |
| type_: Some(ui_input3::KeyEventType::Pressed), |
| ..Default::default() |
| }; |
| let event1_release = ui_input3::KeyEvent { |
| timestamp: Some(0), |
| key: Some(key1), |
| type_: Some(ui_input3::KeyEventType::Released), |
| ..Default::default() |
| }; |
| |
| // Focus client A. |
| focus_ctl.notify(scenic::duplicate_view_ref(&view_ref_a)?).await?; |
| |
| // Press the key and expect client A to receive the event. |
| dispatch_and_expect_key_event(&key_simulator, &mut listener_a, event1_press).await?; |
| |
| assert!(listener_b.next().now_or_never().is_none(), "listener_b should have no events yet"); |
| |
| // Focus client B. |
| // Expect a cancel event for client A and a sync event for the client B. |
| let (focus_result, client_a_event, client_b_event) = future::join3( |
| focus_ctl.notify(scenic::duplicate_view_ref(&view_ref_b)?), |
| expect_key_event(&mut listener_a), |
| expect_key_event(&mut listener_b), |
| ) |
| .await; |
| |
| focus_result?; |
| |
| assert_eq!( |
| ui_input3::KeyEvent { |
| timestamp: Some(0i64), |
| key: Some(input::Key::A), |
| type_: Some(ui_input3::KeyEventType::Cancel), |
| ..Default::default() |
| }, |
| client_a_event |
| ); |
| |
| assert_eq!( |
| ui_input3::KeyEvent { |
| timestamp: Some(0i64), |
| key: Some(input::Key::A), |
| type_: Some(ui_input3::KeyEventType::Sync), |
| ..Default::default() |
| }, |
| client_b_event |
| ); |
| |
| // Release the key and expect client B to receive an event. |
| dispatch_and_expect_key_event(&key_simulator, &mut listener_b, event1_release).await?; |
| |
| assert!(listener_a.next().now_or_never().is_none(), "listener_a should have no more events"); |
| |
| // Focus client A again. |
| focus_ctl.notify(scenic::duplicate_view_ref(&view_ref_a)?).await?; |
| |
| assert!( |
| listener_a.next().now_or_never().is_none(), |
| "listener_a should have no more events after receiving focus" |
| ); |
| |
| Ok(()) |
| } |
| |
| #[fuchsia::test(logging_tags = ["keyboard3_integration_test"])] |
| async fn test_sync_cancel_via_key_event_injector() -> Result<()> { |
| let test_env = TestEnvironment::new().await?; |
| |
| // This test dispatches keys via KeyEventInjector. |
| let key_event_injector = test_env.connect_to_key_event_injector()?; |
| |
| let key_dispatcher = |
| test_helpers::KeyEventInjectorDispatcher { key_event_injector: &key_event_injector }; |
| let key_simulator = test_helpers::KeySimulator::new(&key_dispatcher); |
| |
| let keyboard_service_client_a = test_env.connect_to_keyboard_service().context("client_a")?; |
| |
| let keyboard_service_client_b = test_env.connect_to_keyboard_service().context("client_b")?; |
| |
| test_sync_cancel_with_connections( |
| test_env.connect_to_focus_controller()?, |
| &key_simulator, |
| &keyboard_service_client_a, |
| &keyboard_service_client_b, |
| ) |
| .await |
| } |
| |
| struct TestHandles { |
| _test_env: TestEnvironment, |
| _keyboard_service: ui_input3::KeyboardProxy, |
| listener_stream: ui_input3::KeyboardListenerRequestStream, |
| injector_service: ui_input3::KeyEventInjectorProxy, |
| _view_ref: ui_views::ViewRef, |
| } |
| |
| impl TestHandles { |
| async fn new() -> Result<TestHandles> { |
| let _test_env = TestEnvironment::new().await?; |
| |
| // Create fake client. |
| let (listener_client_end, listener_stream) = create_request_stream::< |
| ui_input3::KeyboardListenerMarker, |
| >() |
| .with_context(|| { |
| format!("create_request_stream for {}", ui_input3::KeyboardListenerMarker::DEBUG_NAME) |
| })?; |
| let _view_ref = scenic::ViewRefPair::new()?.view_ref; |
| |
| let _keyboard_service: ui_input3::KeyboardProxy = |
| _test_env.connect_to_keyboard_service()?; |
| _keyboard_service |
| .add_listener(scenic::duplicate_view_ref(&_view_ref)?, listener_client_end) |
| .await |
| .expect("add_listener"); |
| |
| let focus_controller = _test_env.connect_to_focus_controller()?; |
| focus_controller.notify(scenic::duplicate_view_ref(&_view_ref)?).await?; |
| |
| let injector_service = _test_env.connect_to_key_event_injector()?; |
| |
| Ok(TestHandles { |
| _test_env, |
| _keyboard_service, |
| listener_stream, |
| injector_service, |
| _view_ref, |
| }) |
| } |
| } |
| |
| fn inject_key_and_receive_keyboard_protocol_message( |
| event: ui_input3::KeyEvent, |
| ) -> Result<(Result<ui_input3::KeyEventStatus, fidl::Error>, ui_input3::KeyEvent)> { |
| fasync::TestExecutor::new().run_singlethreaded(async { |
| let mut handles = TestHandles::new().await?; |
| |
| let (was_handled, received_event) = future::join( |
| handles.injector_service.inject(&event), |
| expect_key_event(&mut handles.listener_stream), |
| ) |
| .await; |
| Ok((was_handled, received_event)) |
| }) |
| } |
| |
| #[test_case(input::Key::A, None => (Some(input::Key::A), None); "without_key_meaning")] |
| #[test_case(input::Key::A, 'a' |
| => (Some(input::Key::A), Some(ui_input3::KeyMeaning::Codepoint(0x61))); |
| "key_and_meaning")] |
| #[test_case(None, 'a' |
| => (None, Some(ui_input3::KeyMeaning::Codepoint(0x61))); |
| "only_key_meaning_lower_alpha")] |
| #[test_case(None, 'A' |
| => (None, Some(ui_input3::KeyMeaning::Codepoint(0x41))); |
| "only_key_meaning_upper_alpha")] |
| #[test_case(None, '1' |
| => (None, Some(ui_input3::KeyMeaning::Codepoint(0x31))); |
| "only_key_meaning_numeric")] |
| #[test_case(None, ui_input3::KeyMeaning::NonPrintableKey(ui_input3::NonPrintableKey::Enter) |
| => (Some(input::Key::Enter), |
| Some(ui_input3::KeyMeaning::NonPrintableKey(ui_input3::NonPrintableKey::Enter))); |
| "only_key_meaning_enter")] |
| #[test_case(None, ui_input3::KeyMeaning::NonPrintableKey(ui_input3::NonPrintableKey::Tab) |
| => (Some(input::Key::Tab), |
| Some(ui_input3::KeyMeaning::NonPrintableKey(ui_input3::NonPrintableKey::Tab))); |
| "only_key_meaning_tab")] |
| #[test_case(None, ui_input3::KeyMeaning::NonPrintableKey(ui_input3::NonPrintableKey::Backspace) |
| => (Some(input::Key::Backspace), |
| Some(ui_input3::KeyMeaning::NonPrintableKey(ui_input3::NonPrintableKey::Backspace))); |
| "only_key_meaning_backspace")] |
| #[test_case(input::Key::Down, ui_input3::KeyMeaning::NonPrintableKey(ui_input3::NonPrintableKey::Down) |
| => (Some(input::Key::Down), |
| Some(ui_input3::KeyMeaning::NonPrintableKey(ui_input3::NonPrintableKey::Down))); |
| "arrow_down")] |
| #[test_case(None, ui_input3::KeyMeaning::NonPrintableKey(ui_input3::NonPrintableKey::Down) |
| => (None, |
| Some(ui_input3::KeyMeaning::NonPrintableKey(ui_input3::NonPrintableKey::Down))); |
| "only_key_meaning_arrow_down")] |
| #[test_case(input::Key::Up, ui_input3::KeyMeaning::NonPrintableKey(ui_input3::NonPrintableKey::Up) |
| => (Some(input::Key::Up), |
| Some(ui_input3::KeyMeaning::NonPrintableKey(ui_input3::NonPrintableKey::Up))); |
| "arrow_up")] |
| #[test_case(input::Key::Left, ui_input3::KeyMeaning::NonPrintableKey(ui_input3::NonPrintableKey::Left) |
| => (Some(input::Key::Left), |
| Some(ui_input3::KeyMeaning::NonPrintableKey(ui_input3::NonPrintableKey::Left))); |
| "arrow_left")] |
| #[test_case(input::Key::Right, ui_input3::KeyMeaning::NonPrintableKey(ui_input3::NonPrintableKey::Right) |
| => (Some(input::Key::Right), |
| Some(ui_input3::KeyMeaning::NonPrintableKey(ui_input3::NonPrintableKey::Right))); |
| "arrow_right")] |
| #[test_case(input::Key::End, ui_input3::KeyMeaning::NonPrintableKey(ui_input3::NonPrintableKey::End) |
| => (Some(input::Key::End), |
| Some(ui_input3::KeyMeaning::NonPrintableKey(ui_input3::NonPrintableKey::End))); |
| "end")] |
| #[test_case(input::Key::Home, ui_input3::KeyMeaning::NonPrintableKey(ui_input3::NonPrintableKey::Home) |
| => (Some(input::Key::Home), |
| Some(ui_input3::KeyMeaning::NonPrintableKey(ui_input3::NonPrintableKey::Home))); |
| "home")] |
| #[test_case(input::Key::PageDown, ui_input3::KeyMeaning::NonPrintableKey(ui_input3::NonPrintableKey::PageDown) |
| => (Some(input::Key::PageDown), |
| Some(ui_input3::KeyMeaning::NonPrintableKey(ui_input3::NonPrintableKey::PageDown))); |
| "page_down")] |
| #[test_case(input::Key::PageUp, ui_input3::KeyMeaning::NonPrintableKey(ui_input3::NonPrintableKey::PageUp) |
| => (Some(input::Key::PageUp), |
| Some(ui_input3::KeyMeaning::NonPrintableKey(ui_input3::NonPrintableKey::PageUp))); |
| "page_up")] |
| fn test_inject_key_yields_expected_key_and_key_meaning( |
| key: impl Into<Option<input::Key>>, |
| key_meaning: impl Into<test_helpers::KeyMeaningWrapper>, |
| ) -> (Option<input::Key>, Option<ui_input3::KeyMeaning>) { |
| let (was_handled, received_event) = inject_key_and_receive_keyboard_protocol_message( |
| create_key_event(zx::Time::ZERO, ui_input3::KeyEventType::Pressed, key, None, key_meaning), |
| ) |
| .expect("injection failed"); |
| assert_matches::assert_matches!(was_handled, Ok(ui_input3::KeyEventStatus::Handled)); |
| (received_event.key, received_event.key_meaning) |
| } |