| // 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. |
| |
| use failure::{bail, err_msg, Error, ResultExt}; |
| use fidl_fuchsia_ui_text as txt; |
| use fuchsia_async::TimeoutExt; |
| use futures::prelude::*; |
| use std::collections::HashSet; |
| |
| pub struct TextFieldWrapper { |
| proxy: txt::TextFieldProxy, |
| last_state: txt::TextFieldState, |
| defunct_point_ids: HashSet<u64>, |
| current_point_ids: HashSet<u64>, |
| } |
| |
| /// This wraps the TextFieldProxy, and provides convenient features like storing the last state |
| /// update, various validation functions for tests. It also a great place to add checks that work |
| /// across all tests; for instance, right now, it validates that all TextPoints in all function |
| /// calls are not reused across revisions, and that any distance or contents check works even if |
| /// the range is inverted. |
| impl TextFieldWrapper { |
| /// Creates a new TextFieldWrapper from a proxy. This is a async function and can fail, since it |
| /// waits for the initial state update to come from the TextField. |
| pub async fn new(proxy: txt::TextFieldProxy) -> Result<TextFieldWrapper, Error> { |
| let state = await!(get_update(&proxy)).context("Receiving initial state.")?; |
| Ok(TextFieldWrapper { |
| proxy, |
| current_point_ids: all_point_ids_for_state(&state), |
| defunct_point_ids: HashSet::new(), |
| last_state: state, |
| }) |
| } |
| |
| /// Returns a cloned version of the latest state from the server. To update this, either use one |
| /// of the editing methods on the TextFieldWrapper, or if making calls on the proxy directly, |
| /// call `await!(text_field_wrapper.wait_for_update())` after you expect a new state update from |
| /// the TextField. |
| pub fn state(&self) -> txt::TextFieldState { |
| fn clone_range(range: &txt::TextRange) -> txt::TextRange { |
| txt::TextRange { |
| start: txt::TextPoint { id: range.start.id }, |
| end: txt::TextPoint { id: range.end.id }, |
| } |
| } |
| txt::TextFieldState { |
| document: clone_range(&self.last_state.document), |
| selection: self.last_state.selection.as_ref().map(|selection| { |
| Box::new(txt::TextSelection { |
| range: clone_range(&selection.range), |
| anchor: selection.anchor, |
| affinity: selection.affinity, |
| }) |
| }), |
| composition: self.last_state.composition.as_ref().map(|v| Box::new(clone_range(v))), |
| composition_highlight: self |
| .last_state |
| .composition_highlight |
| .as_ref() |
| .map(|v| Box::new(clone_range(v))), |
| dead_key_highlight: self |
| .last_state |
| .dead_key_highlight |
| .as_ref() |
| .map(|v| Box::new(clone_range(v))), |
| revision: self.last_state.revision, |
| } |
| } |
| |
| /// Waits for an on_update event from the TextFieldProxy, and updates the last state tracked |
| /// by TextFieldWrapper. Edit functions on TextFieldWrapper itself already call this; only |
| /// use it if you're doing something with the TextFieldProxy directly. |
| pub async fn wait_for_update(&mut self) -> Result<(), Error> { |
| self.defunct_point_ids = |
| &self.defunct_point_ids | &all_point_ids_for_state(&self.last_state); |
| self.last_state = match await!(get_update(&self.proxy)) { |
| Ok(v) => v, |
| Err(e) => bail!(format!("{}", e)), |
| }; |
| self.current_point_ids = all_point_ids_for_state(&self.last_state); |
| self.validate_point_ids() |
| } |
| |
| /// An internal function that validates the current_point_ids and defunct_point_ids are |
| /// disjoint sets. If they aren't disjoint, then the TextField incorrectly reused a |
| /// point ID between two revisions. |
| fn validate_point_ids(&mut self) -> Result<(), Error> { |
| let in_both_sets = &self.current_point_ids & &self.defunct_point_ids; |
| if in_both_sets.len() != 0 { |
| let as_strings: Vec<String> = |
| in_both_sets.into_iter().map(|i| format!("{}", i)).collect(); |
| bail!(format!( |
| "Expected TextPoint ids to not be reused between revisions: {}", |
| as_strings.join(", ") |
| )) |
| } |
| Ok(()) |
| } |
| |
| /// Returns a handle to the raw TextFieldProxy, useful for sending it weird unexpected |
| /// input and making sure it responds correctly. |
| pub fn proxy(&self) -> &txt::TextFieldProxy { |
| &self.proxy |
| } |
| |
| /// Inserts text as though the user just typed it, at the caret, replacing any selected text. |
| /// Also waits for an on_update state update event before returning. |
| pub async fn simple_insert<'a>(&mut self, contents: &'static str) -> Result<(), Error> { |
| let rev = self.last_state.revision; |
| self.proxy.begin_edit(rev)?; |
| let selection = match &mut self.last_state.selection { |
| Some(v) => v, |
| None => bail!("Expected state to contain a selection"), |
| }; |
| self.proxy.replace(&mut selection.range, contents)?; |
| if await!(self.proxy.commit_edit())? != txt::TextError::Ok { |
| bail!("Expected commit_edit to succeed"); |
| } |
| await!(self.wait_for_update())?; |
| Ok(()) |
| } |
| |
| /// Returns a new TextPoint offset from the specified one. Use this function instead of |
| /// `text_field_wrapper.proxy().point_offset()` when possible, since it also double checks |
| /// any points returned aren't used across revisions. You may need the proxy's |
| /// point_offset method when giving weird data to the proxy, though, like incorrect |
| /// revision numbers. |
| pub async fn point_offset<'a>( |
| &'a mut self, |
| mut point: &'a mut txt::TextPoint, |
| offset: i64, |
| ) -> Result<txt::TextPoint, Error> { |
| let (new_point, err) = |
| await!(self.proxy.point_offset(&mut point, offset, self.last_state.revision))?; |
| if err != txt::TextError::Ok { |
| bail!(format!("Expected point_offset request to succeed, returned {:?} instead", err)); |
| } |
| self.current_point_ids.insert(new_point.id); |
| if let Err(e) = self.validate_point_ids() { |
| return Err(e); |
| } |
| Ok(new_point) |
| } |
| |
| /// A convenience function that returns the string contents of a range. |
| pub async fn contents<'a>( |
| &'a mut self, |
| range: &'a mut txt::TextRange, |
| ) -> Result<(String, txt::TextPoint), Error> { |
| let (contents, actual_start, err) = |
| await!(self.proxy.contents(range, self.last_state.revision))?; |
| if err != txt::TextError::Ok { |
| bail!(format!("Expected contents request to succeed, returned {:?} instead", err)); |
| } |
| Ok((contents, actual_start)) |
| } |
| |
| /// A convenience function that returns the distance of a range. |
| pub async fn distance<'a>(&'a mut self, range: &'a mut txt::TextRange) -> Result<i64, Error> { |
| let (length, err) = await!(self.proxy.distance(range, self.last_state.revision))?; |
| if err != txt::TextError::Ok { |
| bail!(format!("Expected length request to succeed, returned {:?} instead", err)); |
| } |
| Ok(length) |
| } |
| |
| /// A convenience function that validates that a distance call returns an expected value. Also |
| /// double checks that inverting the range correctly negates the returned distance. |
| pub async fn validate_distance<'a>( |
| &'a mut self, |
| range: &'a txt::TextRange, |
| expected_result: i64, |
| ) -> Result<(), Error> { |
| // try forwards |
| let mut new_range = txt::TextRange { |
| start: txt::TextPoint { id: range.start.id }, |
| end: txt::TextPoint { id: range.end.id }, |
| }; |
| let length = await!(self.distance(&mut new_range))?; |
| if length != expected_result { |
| bail!(format!( |
| "Expected length request to return {:?}, instead got {:?}", |
| expected_result, length |
| )) |
| }; |
| // try backwards |
| let inverted_expected_result = -expected_result; |
| let mut new_range = txt::TextRange { |
| start: txt::TextPoint { id: range.end.id }, |
| end: txt::TextPoint { id: range.start.id }, |
| }; |
| let length = await!(self.distance(&mut new_range))?; |
| if length != inverted_expected_result { |
| bail!(format!( |
| "Expected length request to return {:?}, instead got {:?}", |
| inverted_expected_result, length |
| )) |
| }; |
| Ok(()) |
| } |
| |
| /// A convenience function that validates that a contents call returns an expected value. Also |
| /// double checks that inverting the range correctly returns an identical string. |
| pub async fn validate_contents<'a>( |
| &'a mut self, |
| range: &'a txt::TextRange, |
| expected_result: &'a str, |
| ) -> Result<(), Error> { |
| // try forwards |
| let mut new_range = txt::TextRange { |
| start: txt::TextPoint { id: range.start.id }, |
| end: txt::TextPoint { id: range.end.id }, |
| }; |
| let (contents, _true_start_point) = await!(self.contents(&mut new_range))?; |
| if contents != expected_result { |
| bail!(format!( |
| "Expected contents request to return {:?}, instead got {:?}", |
| expected_result, contents |
| )) |
| }; |
| // try backwards |
| let mut new_range = txt::TextRange { |
| start: txt::TextPoint { id: range.end.id }, |
| end: txt::TextPoint { id: range.start.id }, |
| }; |
| let (contents, _true_start_point) = await!(self.contents(&mut new_range))?; |
| if contents != expected_result { |
| bail!(format!( |
| "Expected contents request to return {:?}, instead got {:?}", |
| expected_result, contents |
| )) |
| }; |
| Ok(()) |
| } |
| } |
| |
| async fn get_update(text_field: &txt::TextFieldProxy) -> Result<txt::TextFieldState, Error> { |
| let mut stream = text_field.take_event_stream(); |
| let msg_future = stream |
| .try_next() |
| .map_err(|e| err_msg(format!("{}", e))) |
| .on_timeout(*crate::TEST_TIMEOUT, || Err(err_msg("Waiting for on_update event timed out"))); |
| let msg = await!(msg_future)?.ok_or(err_msg("TextMgr event stream unexpectedly closed"))?; |
| match msg { |
| txt::TextFieldEvent::OnUpdate { state, .. } => Ok(state), |
| } |
| } |
| |
| fn all_point_ids_for_state(state: &txt::TextFieldState) -> HashSet<u64> { |
| let mut point_ids = HashSet::new(); |
| let mut point_ids_for_range = |range: &txt::TextRange| { |
| point_ids.insert(range.start.id); |
| point_ids.insert(range.end.id); |
| }; |
| state.selection.as_ref().map(|selection| point_ids_for_range(&selection.range)); |
| state.composition.as_ref().map(|range| point_ids_for_range(&*range)); |
| state.composition_highlight.as_ref().map(|range| point_ids_for_range(&*range)); |
| state.dead_key_highlight.as_ref().map(|range| point_ids_for_range(&*range)); |
| point_ids_for_range(&state.document); |
| point_ids |
| } |
| |
| #[cfg(test)] |
| mod test { |
| use super::*; |
| fn default_range(n: u64) -> txt::TextRange { |
| txt::TextRange { start: txt::TextPoint { id: n }, end: txt::TextPoint { id: n + 1 } } |
| } |
| fn default_state(n: u64) -> txt::TextFieldState { |
| txt::TextFieldState { |
| document: default_range(n), |
| selection: Some(Box::new(txt::TextSelection { |
| range: default_range(n + 2), |
| anchor: txt::TextSelectionAnchor::AnchoredAtStart, |
| affinity: txt::TextAffinity::Upstream, |
| })), |
| composition: None, |
| composition_highlight: None, |
| dead_key_highlight: None, |
| revision: n + 4, |
| } |
| } |
| |
| #[fuchsia_async::run_singlethreaded] |
| #[test] |
| async fn test_wrapper_insert() { |
| let (proxy, server_end) = fidl::endpoints::create_proxy::<txt::TextFieldMarker>() |
| .expect("Should have created proxy"); |
| let (mut stream, control_handle) = server_end |
| .into_stream_and_control_handle() |
| .expect("Should have created stream and control handle"); |
| control_handle.send_on_update(&mut default_state(0)).expect("Should have sent update"); |
| fuchsia_async::spawn( |
| async { |
| let mut wrapper = await!(TextFieldWrapper::new(proxy)) |
| .expect("Should have created text field wrapper"); |
| await!(wrapper.simple_insert("meow!")).expect("Should have inserted successfully"); |
| }, |
| ); |
| if let txt::TextFieldRequest::BeginEdit { revision, .. } = await!(stream.try_next()) |
| .expect("Waiting for message failed") |
| .expect("Should have sent message") |
| { |
| assert_eq!(revision, 4); |
| } else { |
| panic!("Expected BeginEdit"); |
| } |
| if let txt::TextFieldRequest::Replace { new_text, .. } = await!(stream.try_next()) |
| .expect("Waiting for message failed") |
| .expect("Should have sent message") |
| { |
| assert_eq!(new_text, "meow!"); |
| } else { |
| panic!("Expected Replace"); |
| } |
| if let txt::TextFieldRequest::CommitEdit { .. } = await!(stream.try_next()) |
| .expect("Waiting for message failed") |
| .expect("Should have sent message") |
| { |
| // ok! |
| } else { |
| panic!("Expected CommitEdit"); |
| } |
| assert!(true); |
| } |
| |
| #[fuchsia_async::run_singlethreaded] |
| #[test] |
| async fn test_duplicate_points_cause_error() { |
| let (proxy, server_end) = fidl::endpoints::create_proxy::<txt::TextFieldMarker>() |
| .expect("Should have created proxy"); |
| let (_stream, control_handle) = server_end |
| .into_stream_and_control_handle() |
| .expect("Should have created stream and control handle"); |
| control_handle.send_on_update(&mut default_state(0)).expect("Should have sent update"); |
| let mut wrapper = |
| await!(TextFieldWrapper::new(proxy)).expect("Should have created text field wrapper"); |
| |
| // send a valid update and make sure it works as expected |
| let mut state = default_state(10); |
| control_handle.send_on_update(&mut state).expect("Should have sent update"); |
| let res = await!(wrapper.wait_for_update()); |
| assert!(res.is_ok()); |
| assert_eq!(wrapper.state().document.start.id, 10); |
| |
| // send an update with the same points but an incremented revision |
| state.revision += 1; |
| control_handle.send_on_update(&mut state).expect("Should have sent update"); |
| let res = await!(wrapper.wait_for_update()); |
| assert!(res.is_err()); // should fail since some points were reused |
| } |
| } |