use edit_session::{Change, EditSession, FinishError};
#[test]
fn many_previews_produce_one_final_change() {
let mut draft = EditSession::new(100, ("photo", "layer-1", 9));
for value in 0..1000 {
assert_eq!(draft.propose::<()>(Ok(value)), Ok(()));
assert_eq!(*draft.original(), 100);
}
assert_eq!(
draft.finish(&100, &("photo", "layer-1", 9)),
Ok(Some(Change {
before: 100,
after: 999
}))
);
}
#[test]
fn invalid_input_retains_preview_but_blocks_commit_until_corrected() {
let mut draft = EditSession::new(10, 0);
assert_eq!(draft.propose::<&str>(Ok(12)), Ok(()));
assert_eq!(
draft.propose(Err("incomplete number")),
Err("incomplete number")
);
assert_eq!(*draft.preview(), 12);
assert!(!draft.is_valid());
assert_eq!(draft.check(&10, &0), Err(FinishError::InvalidProposal));
assert_eq!(draft.propose::<&str>(Ok(14)), Ok(()));
assert_eq!(
draft.finish(&10, &0),
Ok(Some(Change {
before: 10,
after: 14
}))
);
}
#[test]
fn unchanged_and_return_to_original_have_no_commit() {
assert_eq!(EditSession::new(20, 0).finish(&20, &0), Ok(None));
let mut draft = EditSession::new(20, 0);
assert_eq!(draft.propose::<()>(Ok(50)), Ok(()));
assert_eq!(draft.propose::<()>(Ok(20)), Ok(()));
assert_eq!(draft.finish(&20, &0), Ok(None));
}
#[test]
fn stale_document_target_or_revision_cannot_commit() {
for context in [(2, 5, 8), (1, 6, 8), (1, 5, 9)] {
let draft = EditSession::new(100, (1, 5, 8));
assert_eq!(
draft.finish(&100, &context),
Err(FinishError::ContextChanged)
);
}
}
#[test]
fn unversioned_value_change_is_also_rejected() {
assert_eq!(
EditSession::new(100, 1).finish(&50, &1),
Err(FinishError::ValueChanged)
);
}
#[test]
fn undo_back_to_equal_value_is_stale_when_revision_has_advanced() {
assert_eq!(
EditSession::new(100, 1).finish(&100, &3),
Err(FinishError::ContextChanged)
);
}
#[test]
fn invalid_final_input_does_not_silently_commit_last_valid_value() {
let mut draft = EditSession::new(10, 0);
assert_eq!(draft.propose::<&str>(Ok(12)), Ok(()));
assert!(draft.propose(Err("not finite")).is_err());
assert_eq!(draft.finish(&10, &0), Err(FinishError::InvalidProposal));
}
#[test]
fn optional_values_preserve_inheritance_instead_of_flattening_it() {
let mut draft = EditSession::new(None::<u16>, 0);
assert_eq!(draft.propose::<()>(Ok(Some(20))), Ok(()));
assert_eq!(draft.propose::<()>(Ok(None)), Ok(()));
assert_eq!(draft.finish(&None, &0), Ok(None));
}
#[test]
fn cancel_does_not_touch_host_value() {
let live = String::from("original");
let mut draft = EditSession::new(live.clone(), 1);
assert_eq!(draft.propose::<()>(Ok("preview".into())), Ok(()));
draft.cancel();
assert_eq!(live, "original");
}
#[test]
fn invalid_input_can_remain_editable_while_live_context_is_current() {
let mut draft = EditSession::new(10, 7);
assert!(draft.propose(Err("incomplete number")).is_err());
assert_eq!(draft.check_live(&10, &7), Ok(()));
assert_eq!(draft.check(&10, &7), Err(FinishError::InvalidProposal));
for (live, context, error) in [
(10, 8, FinishError::ContextChanged),
(11, 8, FinishError::ContextChanged),
(11, 7, FinishError::ValueChanged),
] {
assert_eq!(draft.check_live(&live, &context), Err(error));
assert_eq!(draft.check(&live, &context), Err(error));
}
assert!(!draft.is_valid());
}
#[test]
fn reset_clears_invalid_input_and_confirming_it_is_a_noop() {
#[derive(Debug, PartialEq)]
struct Context(u64);
let mut draft = EditSession::new(String::from("original"), Context(7));
assert_eq!(draft.propose::<()>(Ok(String::from("preview"))), Ok(()));
assert!(draft.propose(Err("invalid")).is_err());
draft.reset();
assert!(draft.is_valid());
assert_eq!(draft.preview(), "original");
assert_eq!(draft.original(), "original");
assert_eq!(draft.context(), &Context(7));
assert_eq!(
draft.finish(&String::from("original"), &Context(7)),
Ok(None)
);
}
#[test]
fn reset_allows_further_editing_without_changing_the_original() {
let mut draft = EditSession::new(10, 7);
assert_eq!(draft.propose::<()>(Ok(12)), Ok(()));
draft.reset();
draft.reset();
assert_eq!(draft.propose::<()>(Ok(15)), Ok(()));
assert_eq!(
draft.finish(&10, &7),
Ok(Some(Change {
before: 10,
after: 15
}))
);
}
#[test]
fn reset_does_not_rebase_a_stale_draft_or_hide_a_live_value_conflict() {
for (live, context, error) in [
(10, 9, FinishError::ContextChanged), (12, 7, FinishError::ValueChanged),
] {
let mut draft = EditSession::new(10, 7);
assert_eq!(draft.propose::<()>(Ok(12)), Ok(()));
draft.reset();
assert_eq!(draft.finish(&live, &context), Err(error));
}
}
#[test]
fn finish_errors_support_standard_error_reporting() {
for (error, message) in [
(
FinishError::InvalidProposal,
"the latest proposal is invalid",
),
(FinishError::ContextChanged, "the edit context has changed"),
(FinishError::ValueChanged, "the live value has changed"),
] {
let reported: &dyn core::error::Error = &error;
assert_eq!(reported.to_string(), message);
assert!(reported.source().is_none());
}
}
#[test]
fn owned_form_fields_remain_isolated_until_one_change_is_finished() {
#[derive(Clone, Debug, PartialEq, Eq)]
struct Fields {
title: String,
tags: Vec<String>,
inherited_size: Option<u16>,
}
let live = Fields {
title: "original".into(),
tags: vec!["keep".into()],
inherited_size: None,
};
let mut draft = EditSession::new(live.clone(), 7);
let mut proposal = draft.preview().clone();
proposal.title.push_str(" edited");
proposal.tags.push("new".into());
proposal.inherited_size = Some(12);
assert_eq!(draft.propose::<()>(Ok(proposal.clone())), Ok(()));
assert_eq!(draft.original(), &live);
assert_eq!(live.title, "original");
assert_eq!(live.tags, ["keep"]);
assert_eq!(live.inherited_size, None);
assert!(draft.propose(Err("incomplete field")).is_err());
assert_eq!(draft.preview(), &proposal);
assert_eq!(draft.check(&live, &7), Err(FinishError::InvalidProposal));
assert_eq!(draft.propose::<()>(Ok(proposal.clone())), Ok(()));
assert_eq!(
draft.finish(&live, &7),
Ok(Some(Change {
before: live,
after: proposal,
}))
);
}
#[test]
fn proposing_and_checking_do_not_clone_snapshots_per_event() {
use std::cell::Cell;
struct Value<'a> {
number: u16,
clones: &'a Cell<usize>,
}
impl Clone for Value<'_> {
fn clone(&self) -> Self {
self.clones.set(self.clones.get() + 1);
Self {
number: self.number,
clones: self.clones,
}
}
}
impl PartialEq for Value<'_> {
fn eq(&self, other: &Self) -> bool {
self.number == other.number
}
}
let clones = Cell::new(0);
let live = Value {
number: 0,
clones: &clones,
};
let mut draft = EditSession::new(
Value {
number: 0,
clones: &clones,
},
7,
);
assert_eq!(clones.get(), 1);
for number in 1..1000 {
assert!(
draft
.propose::<()>(Ok(Value {
number,
clones: &clones
}))
.is_ok()
);
assert!(draft.check(&live, &7).is_ok());
}
assert_eq!(clones.get(), 1);
draft.reset();
assert_eq!(clones.get(), 2);
assert!(matches!(draft.finish(&live, &7), Ok(None)));
assert_eq!(clones.get(), 2);
}