| // Copyright 2022 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. |
| |
| use { |
| anyhow::Error, |
| fidl::endpoints::{create_proxy, create_proxy_and_stream, create_request_stream}, |
| fidl_fuchsia_element as felement, |
| fidl_fuchsia_input::Key, |
| fidl_fuchsia_ui_app as ui_app, |
| fidl_fuchsia_ui_input3::{KeyEvent, KeyEventStatus}, |
| fidl_fuchsia_ui_test_input as ui_test_input, fidl_fuchsia_ui_test_scene as ui_test_scene, |
| fuchsia_async as fasync, |
| fuchsia_component::client::connect_to_protocol, |
| fuchsia_scenic::flatland::ViewCreationTokenPair, |
| futures::future::{AbortHandle, Abortable}, |
| futures::{StreamExt, TryStreamExt}, |
| std::collections::HashMap, |
| tracing::*, |
| }; |
| |
| use crate::{ |
| child_view::{ChildView, ChildViewId}, |
| event::{ChildViewEvent, Event, SystemEvent, ViewSpecHolder, WindowEvent}, |
| utils::EventSender, |
| window::{Window, WindowId}, |
| }; |
| |
| #[derive(Debug)] |
| enum TestEvent {} |
| |
| struct TestApp<T> { |
| width: u32, |
| height: u32, |
| active_window: Option<WindowId>, |
| windows: HashMap<WindowId, Window<T>>, |
| child_views: HashMap<ChildViewId, ChildView<T>>, |
| } |
| |
| impl<T> TestApp<T> { |
| pub fn new() -> Self { |
| TestApp { |
| width: 0, |
| height: 0, |
| active_window: None, |
| windows: HashMap::new(), |
| child_views: HashMap::new(), |
| } |
| } |
| } |
| |
| #[fuchsia::test] |
| async fn test_appkit() -> Result<(), Error> { |
| let (event_sender, mut receiver) = EventSender::<TestEvent>::new(); |
| |
| let (graphical_presenter_proxy, graphical_presenter_request_stream) = |
| create_proxy_and_stream::<felement::GraphicalPresenterMarker>()?; |
| |
| let (services_abort, services_registration) = AbortHandle::new_pair(); |
| let services_fut = Abortable::new( |
| start_services(event_sender.clone(), graphical_presenter_request_stream), |
| services_registration, |
| ); |
| |
| let mut app = TestApp::new(); |
| // Declare an event handler that does not allow blocking async calls within it. |
| let mut event_handler = |event| { |
| info!("------ParentView {:?}", event); |
| match event { |
| Event::Init => {} |
| Event::WindowEvent { window_id: id, event: window_event } => match window_event { |
| WindowEvent::Resized { width, height } => { |
| app.width = width; |
| app.height = height; |
| if app.active_window.is_none() { |
| app.active_window = Some(id); |
| |
| let cloned_graphical_presenter = graphical_presenter_proxy.clone(); |
| let cloned_event_sender = event_sender.clone(); |
| fasync::Task::spawn(async move { |
| create_child_view_spec(cloned_graphical_presenter, cloned_event_sender) |
| .await |
| .expect("Failed to create_child_view"); |
| }) |
| .detach(); |
| } |
| } |
| WindowEvent::NeedsRedraw { .. } => { |
| assert!( |
| app.width > 0 && app.height > 0, |
| "Redraw event received before window was resized" |
| ); |
| } |
| WindowEvent::Keyboard { event, responder } => { |
| if let KeyEvent { key: Some(Key::Q), .. } = event { |
| event_sender.send(Event::Exit).expect("Failed to send Event::Exit event"); |
| responder |
| .send(KeyEventStatus::Handled) |
| .expect("Failed to respond to keyboard event"); |
| } else { |
| responder |
| .send(KeyEventStatus::NotHandled) |
| .expect("Failed to respond to keyboard event"); |
| } |
| } |
| _ => {} |
| }, |
| Event::SystemEvent { event: system_event } => match system_event { |
| SystemEvent::ViewCreationToken { token: view_creation_token } => { |
| let mut window = Window::new(event_sender.clone()) |
| .with_view_creation_token(view_creation_token); |
| window.create_view().expect("Failed to create view for window"); |
| app.windows.insert(window.id(), window); |
| } |
| SystemEvent::PresentViewSpec { view_spec_holder } => { |
| let window = app.windows.get_mut(&app.active_window.unwrap()).unwrap(); |
| let child_view = window |
| .create_child_view( |
| view_spec_holder, |
| app.width, |
| app.height, |
| event_sender.clone(), |
| ) |
| .expect("Failed to create child view"); |
| app.child_views.insert(child_view.id(), child_view); |
| } |
| }, |
| Event::ChildViewEvent { child_view_id, window_id, event: child_view_event } => { |
| let window = app.windows.get_mut(&window_id).unwrap(); |
| let child_view = app.child_views.get_mut(&child_view_id).unwrap(); |
| |
| match child_view_event { |
| ChildViewEvent::Available => { |
| window.set_content( |
| window.get_root_transform_id(), |
| child_view.get_content_id(), |
| ); |
| window.redraw(); |
| } |
| ChildViewEvent::Attached { view_ref } => { |
| // Set focus to child view. |
| window.request_focus(view_ref); |
| } |
| ChildViewEvent::Detached => {} |
| } |
| } |
| Event::Exit => { |
| app.windows.clear(); |
| services_abort.abort(); |
| } |
| _ => {} |
| } |
| }; |
| let loop_fut = async move { |
| while let Some(event) = receiver.next().await { |
| let should_exit = matches!(event, Event::Exit); |
| event_handler(event); |
| if should_exit { |
| break; |
| } |
| } |
| }; |
| let _ = futures::join!(loop_fut, services_fut); |
| Ok(()) |
| } |
| |
| async fn start_services( |
| event_sender: EventSender<TestEvent>, |
| graphical_presenter_request_stream: felement::GraphicalPresenterRequestStream, |
| ) { |
| let view_provider_fut = start_view_provider(event_sender.clone()); |
| let graphical_presenter_fut = |
| start_graphical_presenter(event_sender.clone(), graphical_presenter_request_stream); |
| |
| futures::join!(view_provider_fut, graphical_presenter_fut); |
| } |
| |
| async fn start_view_provider(event_sender: EventSender<TestEvent>) { |
| let (view_provider, mut view_provider_request_stream) = |
| create_request_stream::<ui_app::ViewProviderMarker>() |
| .expect("failed to create ViewProvider request stream"); |
| |
| let scene_provider_fut = async move { |
| let scene_provider = connect_to_protocol::<ui_test_scene::ControllerMarker>() |
| .expect("failed to connect to fuchsia.ui.test.scene.Controller"); |
| let _view_ref_koid = scene_provider |
| .attach_client_view(ui_test_scene::ControllerAttachClientViewRequest { |
| view_provider: Some(view_provider), |
| ..ui_test_scene::ControllerAttachClientViewRequest::EMPTY |
| }) |
| .await |
| .expect("failed to attach root client view"); |
| }; |
| |
| let view_provider_fut = async move { |
| match view_provider_request_stream |
| .next() |
| .await |
| .expect("Failed to read ViewProvider request stream") |
| { |
| Ok(ui_app::ViewProviderRequest::CreateView2 { args, .. }) => { |
| event_sender |
| .send(Event::SystemEvent { |
| event: SystemEvent::ViewCreationToken { |
| token: args.view_creation_token.unwrap(), |
| }, |
| }) |
| .expect("Failed to send SystemEvent::ViewCreationToken event"); |
| } |
| // Panic for all other CreateView requests and errors to fail the test. |
| _ => panic!("ViewProvider impl only handles CreateView2()"), |
| } |
| }; |
| |
| futures::join!(scene_provider_fut, view_provider_fut); |
| } |
| |
| async fn start_graphical_presenter( |
| event_sender: EventSender<TestEvent>, |
| mut request_stream: felement::GraphicalPresenterRequestStream, |
| ) { |
| while let Some(request) = request_stream |
| .try_next() |
| .await |
| .expect("Failed to obtain next GraphicalPresenter request from stream") |
| { |
| match request { |
| felement::GraphicalPresenterRequest::PresentView { |
| view_spec, |
| annotation_controller, |
| view_controller_request, |
| responder, |
| } => { |
| event_sender |
| .send(Event::SystemEvent { |
| event: SystemEvent::PresentViewSpec { |
| view_spec_holder: ViewSpecHolder { |
| view_spec, |
| annotation_controller, |
| view_controller_request, |
| responder: Some(responder), |
| }, |
| }, |
| }) |
| .expect("Failed to send SystemEvent::PresentViewSpec event"); |
| } |
| } |
| } |
| } |
| |
| async fn create_child_view_spec( |
| graphical_presenter: felement::GraphicalPresenterProxy, |
| parent_sender: EventSender<TestEvent>, |
| ) -> Result<(), Error> { |
| let ViewCreationTokenPair { view_creation_token, viewport_creation_token } = |
| ViewCreationTokenPair::new()?; |
| let view_spec = felement::ViewSpec { |
| viewport_creation_token: Some(viewport_creation_token), |
| ..felement::ViewSpec::EMPTY |
| }; |
| let _ = graphical_presenter.present_view(view_spec, None, None).await; |
| |
| let (keyboard, keyboard_server) = create_proxy::<ui_test_input::KeyboardMarker>()?; |
| let input_registry = connect_to_protocol::<ui_test_input::RegistryMarker>()?; |
| input_registry |
| .register_keyboard(ui_test_input::RegistryRegisterKeyboardRequest { |
| device: Some(keyboard_server), |
| ..ui_test_input::RegistryRegisterKeyboardRequest::EMPTY |
| }) |
| .await?; |
| |
| let (mouse, mouse_server) = create_proxy::<ui_test_input::MouseMarker>()?; |
| input_registry |
| .register_mouse(ui_test_input::RegistryRegisterMouseRequest { |
| device: Some(mouse_server), |
| ..ui_test_input::RegistryRegisterMouseRequest::EMPTY |
| }) |
| .await?; |
| |
| fasync::Task::local(async move { |
| let (sender, mut receiver) = futures::channel::mpsc::unbounded::<Event<TestEvent>>(); |
| let event_sender = EventSender::<TestEvent>(sender); |
| event_sender.send(Event::Init).expect("Failed to send Event::Init event"); |
| |
| let mut _window_holder: Option<Window<TestEvent>> = None; |
| let mut view_creation_token = Some(view_creation_token); |
| while let Some(event) = receiver.next().await { |
| info!("------ChildView {:?}", event); |
| match event { |
| Event::Init => { |
| let mut window = Window::new(event_sender.clone()) |
| .with_view_creation_token(view_creation_token.take().unwrap()); |
| window.create_view().expect("Failed to create window for child view"); |
| _window_holder = Some(window); |
| } |
| Event::WindowEvent { event: window_event, .. } => match window_event { |
| WindowEvent::Focused { focused } => { |
| if focused { |
| tap(mouse.clone()); |
| } |
| } |
| WindowEvent::Mouse { .. } => { |
| // Inject 'q' to quit. |
| inject_text("q".to_string(), keyboard.clone()); |
| } |
| WindowEvent::Keyboard { event, responder } => { |
| if let KeyEvent { key: Some(Key::Q), .. } = event { |
| parent_sender |
| .send(Event::Exit) |
| .expect("Failed to send Event::Exit event"); |
| responder |
| .send(KeyEventStatus::Handled) |
| .expect("Failed to respond to keyboard event"); |
| } else { |
| responder |
| .send(KeyEventStatus::NotHandled) |
| .expect("Failed to respond to keyboard event"); |
| } |
| } |
| _ => {} |
| }, |
| _ => {} |
| } |
| } |
| }) |
| .detach(); |
| |
| Ok(()) |
| } |
| |
| fn inject_text(text: String, keyboard: ui_test_input::KeyboardProxy) { |
| fasync::Task::local(async move { |
| keyboard |
| .simulate_us_ascii_text_entry(ui_test_input::KeyboardSimulateUsAsciiTextEntryRequest { |
| text: Some(text), |
| ..ui_test_input::KeyboardSimulateUsAsciiTextEntryRequest::EMPTY |
| }) |
| .await |
| .expect("Failed to inject text using fuchsia.ui.test.input.Keyboard"); |
| }) |
| .detach(); |
| } |
| |
| fn tap(mouse: ui_test_input::MouseProxy) { |
| fasync::Task::local(async move { |
| mouse |
| .simulate_mouse_event(ui_test_input::MouseSimulateMouseEventRequest { |
| pressed_buttons: Some(vec![ui_test_input::MouseButton::First]), |
| ..ui_test_input::MouseSimulateMouseEventRequest::EMPTY |
| }) |
| .await |
| .expect("Failed to tap using fuchsia.ui.test.input.Mouse"); |
| mouse |
| .simulate_mouse_event(ui_test_input::MouseSimulateMouseEventRequest { |
| ..ui_test_input::MouseSimulateMouseEventRequest::EMPTY |
| }) |
| .await |
| .expect("Failed to tap using fuchsia.ui.test.input.Mouse"); |
| }) |
| .detach(); |
| } |