Files
ARCADIA-Studio/crates/oa-desktop/src/app/reference_authoring/tests.rs
T

587 lines
20 KiB
Rust

use super::*;
use crate::forms::Draft;
use oa_core::*;
use serde_json::{Value, json};
const OWNER: Id = Id::from_u128(100);
const BASE: u128 = 10_000;
const PATH: &str = "/body/Oa/Shared/DataType/definition/Record/supertype";
fn element(p: &mut Project, id: Id, kind: Kind, layer: Layer, name: &str, parent: Option<Id>) {
let generated = p.add_element(kind, layer, name, parent).unwrap();
let mut element = p.elements.remove(&generated).unwrap();
element.id = id;
element.alias = format!("object_{}", id.as_u128());
if let Some(SharedBody::DataType { definition, .. }) = element.shared_mut() {
*definition = DataTypeDefinition::Record { supertype: None };
}
p.elements.insert(id, element);
}
fn catalog() -> Project {
let mut p = Project::empty();
p.id = Id::from_u128(1);
p.name = "Reference catalog".into();
element(
&mut p,
OWNER,
Kind::DataType,
Layer::Oa,
"Edited record",
None,
);
for n in (0..1_000).rev() {
let id = Id::from_u128(BASE + n);
element(&mut p, id, Kind::DataType, Layer::Oa, "Controller", None);
p.elements.get_mut(&id).unwrap().alias = format!("controller_{n:04}");
}
element(
&mut p,
Id::from_u128(200),
Kind::DataType,
Layer::Sa,
"Controller",
None,
);
element(
&mut p,
Id::from_u128(201),
Kind::Activity,
Layer::Oa,
"Controller",
None,
);
p
}
fn inspector(p: Project, selected: Id) -> App {
let mut app = App::with_project(p);
let _ = app.update(Message::Select(selected));
app
}
fn request(app: &App, host: &Host, path: &str) -> adapter::Open {
app.reference_draft(host, path).unwrap().reference_open(
app.project(),
path,
Rectangle {
x: 790.,
y: 640.,
width: 278.,
height: 32.,
},
)
}
fn open(app: &mut App, host: &Host, path: &str) {
let request = request(app, host, path);
let message = match host {
Host::Inspector => Message::FieldReferenceOpen(request),
Host::Review { address, facet } => {
Message::Workflow(workflow::Event::semantic(semantic_review::Event::Row {
address: *address,
facet: Some(facet.clone()),
event: RowEvent::ReferenceOpen(request),
}))
}
};
let _ = app.update(message);
assert!(
app.reference_authoring.opened.is_some(),
"field must open: {}",
app.status
);
}
fn event(app: &App, action: Action) -> Message {
let opened = app.reference_authoring.opened.as_ref().unwrap();
opened.host.message(opened.chooser.event(action))
}
fn search(app: &mut App, query: &str) {
let serial = app
.reference_authoring
.opened
.as_ref()
.unwrap()
.chooser
.query_serial()
+ 1;
let event = event(
app,
Action::Query {
text: query.into(),
serial,
},
);
let _ = app.update(event);
}
fn choose(app: &mut App, id: Id) {
let event = event(app, Action::Choose(id));
let _ = app.update(event);
}
fn raw(app: &App) -> Value {
serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap()
}
fn review(app: &App) -> &semantic_review::State {
let Some(workflow::Workflow::Semantic(state)) = app.workflow.as_ref() else {
panic!("Review is open")
};
state
}
fn send_review(app: &mut App, event: RowEvent) {
let addressed = review(app).address_event(event);
let _ = app.update(Message::Workflow(workflow::Event::semantic(addressed)));
}
fn review_host(app: &App) -> Host {
let semantic_review::Event::Row {
address,
facet: Some(facet),
..
} = review(app).address_event(RowEvent::Preview)
else {
panic!("Active facet")
};
Host::Review { address, facet }
}
fn trace(p: &mut Project, id: Id, oa: Id, sa: Id) {
p.traces.insert(
id,
Trace {
id,
alias: format!("trace_{}", id.as_u128()),
name: "Rationale".into(),
description: String::new(),
oa,
sa,
kind: TraceKind::Manual,
baseline: None,
},
);
}
#[test]
fn inspector_reference_browse_is_read_only_then_one_scoped_value_applies_with_exact_undo_redo() {
let p = catalog();
let mut app = inspector(p.clone(), OWNER);
let before = app.document.bundle().clone();
let revision = app.document.revision();
// Malformed escaped text is unrelated to the selected reference. Browsing
// and the scoped write must keep its exact spelling and inactive payload.
app.draft.as_mut().unwrap().set_exact(true).unwrap();
let _ = app.update(Message::Field("/name".into(), "\"unfinished".into()));
let raw_before = raw(&app);
open(&mut app, &Host::Inspector, PATH);
assert_eq!(
app.reference_authoring
.opened
.as_ref()
.unwrap()
.chooser
.count(),
1_000
);
search(&mut app, "controller @controller_0720 OA");
let picker = &app.reference_authoring.opened.as_ref().unwrap().chooser;
assert_eq!(picker.count(), 1);
assert_eq!(picker.highlighted().unwrap().id, Id::from_u128(BASE + 720));
assert_eq!(raw(&app), raw_before);
assert_eq!(app.document.bundle(), &before);
assert_eq!(app.document.revision(), revision);
assert!(!app.document.can_undo());
// Captured search shortcuts cannot accidentally save/apply or undo a model.
let _ = app.update(Message::Save(false));
let _ = app.update(Message::ShortcutUndo(false));
assert_eq!(raw(&app), raw_before);
assert_eq!(app.document.bundle(), &before);
choose(&mut app, Id::from_u128(BASE + 720));
let mut expected_raw = raw_before;
*expected_raw["value"].pointer_mut(PATH).unwrap() = json!(Id::from_u128(BASE + 720));
expected_raw["inputs"][PATH] = json!(Id::from_u128(BASE + 720).to_string());
assert_eq!(raw(&app), expected_raw);
assert_eq!(app.document.bundle(), &before);
assert!(!app.document.can_undo());
let _ = app.update(Message::Apply);
assert_eq!(
app.document.bundle(),
&before,
"Unrelated invalid text remains an Apply error"
);
let _ = app.update(Message::Field("/name".into(), "\"Edited record\"".into()));
let _ = app.update(Message::Apply);
let mut expected = p;
if let Some(SharedBody::DataType { definition, .. }) =
expected.elements.get_mut(&OWNER).unwrap().shared_mut()
{
*definition = DataTypeDefinition::Record {
supertype: Some(Id::from_u128(BASE + 720)),
};
}
assert_eq!(app.project(), &expected);
let accepted = app.document.bundle().clone();
assert_ne!(accepted, before);
let _ = app.update(Message::Undo);
assert_eq!(app.document.bundle(), &before);
let _ = app.update(Message::Redo);
assert_eq!(app.document.bundle(), &accepted);
assert_eq!(app.project(), &expected);
}
#[test]
fn inspector_reference_retains_invalid_missing_and_incompatible_current_until_explicit_clear() {
for (raw_id, expected) in [
("-".into(), "invalid"),
(Id::from_u128(999).to_string(), "missing"),
(Id::from_u128(200).to_string(), "incompatible"),
] {
let mut app = inspector(catalog(), OWNER);
app.draft
.as_mut()
.unwrap()
.shape_scoped(PATH, json!(raw_id))
.unwrap();
let original = raw(&app);
let accepted = app.document.bundle().clone();
open(&mut app, &Host::Inspector, PATH);
let current = &app
.reference_authoring
.opened
.as_ref()
.unwrap()
.chooser
.current;
assert!(matches!(
(expected, current),
("invalid", chooser::Current::InvalidRaw(_))
| ("missing", chooser::Current::Missing(_))
| ("incompatible", chooser::Current::Incompatible(_))
));
let _ = app.update(Message::Escape);
assert_eq!(raw(&app), original);
open(&mut app, &Host::Inspector, PATH);
let clear = event(&app, Action::Clear);
let _ = app.update(clear);
assert!(
app.draft
.as_ref()
.unwrap()
.value
.pointer(PATH)
.unwrap()
.is_null()
);
assert_eq!(app.document.bundle(), &accepted);
assert!(!app.document.can_undo());
}
}
#[test]
fn chooser_rejects_old_results_after_context_recovery_reload_or_modal_and_keeps_history_text_out() {
for change in 0..7 {
let mut app = inspector(catalog(), OWNER);
open(&mut app, &Host::Inspector, PATH);
let delayed = event(&app, Action::Choose(Id::from_u128(BASE + 1)));
match change {
0 => {
app.draft
.as_mut()
.unwrap()
.shape_scoped(PATH, json!(Id::from_u128(BASE + 2)))
.unwrap();
}
1 => {
let saved = app.draft.as_ref().unwrap().recovery_snapshot();
app.draft = Some(Draft::from_recovery(app.project(), saved).unwrap());
}
2 => {
app.canvas_document = Id::new_v4();
}
3 => {
app.hosted.visible = true;
}
4 => {
app.draft
.as_mut()
.unwrap()
.set_context(Some(json!({"different":"scope"})));
}
5 => {
let _ = app.update(Message::Command(Command::Rename(
Id::from_u128(BASE + 3),
"Changed model".into(),
)));
}
_ => {
app.source_workspace.edit(
"unfinished source".into(),
iced::widget::text_editor::Cursor {
position: iced::widget::text_editor::Position { line: 0, column: 0 },
selection: None,
},
);
}
}
let expected = raw(&app);
let source = app.document.bundle().clone();
let _ = app.update(delayed);
assert!(app.reference_authoring.opened.is_none());
assert_eq!(raw(&app), expected);
assert_eq!(app.document.bundle(), &source);
}
let mut p = catalog();
trace(&mut p, Id::from_u128(300), OWNER, Id::from_u128(200));
let app = inspector(p, Id::from_u128(300));
let draft = app.draft.as_ref().unwrap();
let request = draft.reference_open(
app.project(),
"/baseline/semantic/facets/0/references/0/target",
Rectangle::default(),
);
assert!(draft.reference_catalog(app.project(), &request, 0).is_err());
}
#[test]
fn reference_array_row_reorder_and_constructor_switch_invalidate_exact_old_field() {
let mut p = Project::empty();
for n in 0..3 {
element(
&mut p,
Id::from_u128(BASE + n),
Kind::ExchangeItem,
Layer::Oa,
"Signal item",
None,
);
}
element(&mut p, OWNER, Kind::Event, Layer::Oa, "Signal", None);
*p.elements.get_mut(&OWNER).unwrap().shared_mut().unwrap() = SharedBody::Event {
body: EventBody::Signal {
items: vec![Id::from_u128(BASE), Id::from_u128(BASE + 1)],
},
};
let mut app = inspector(p, OWNER);
let list = "/body/Oa/Shared/Event/body/Signal/items";
let path = format!("{list}/0");
open(&mut app, &Host::Inspector, &path);
let delayed = event(&app, Action::Choose(Id::from_u128(BASE + 2)));
let _ = app.update(Message::FieldArray(list.into(), Some(0), 1));
let expected = raw(&app);
assert_eq!(
app.draft.as_ref().unwrap().value.pointer(list),
Some(&json!([Id::from_u128(BASE + 1), Id::from_u128(BASE)]))
);
let _ = app.update(delayed);
assert_eq!(raw(&app), expected);
open(&mut app, &Host::Inspector, &path);
let delayed = event(&app, Action::Choose(Id::from_u128(BASE + 2)));
let _ = app.update(Message::FieldVariant(
"/body/Oa/Shared/Event/body".into(),
json!({"Change":{"expression":"unfinished"}}),
));
let expected = raw(&app);
let _ = app.update(delayed);
assert_eq!(raw(&app), expected);
assert!(!app.document.can_undo());
}
#[test]
fn review_reference_facets_groups_and_attachment_hosts_browse_without_replanning_and_reject_old_facet()
{
for mode in 0..3 {
let mut p = Project::empty();
p.id = Id::from_u128(1);
for (id, layer, parent) in [
(100, Layer::Oa, None),
(101, Layer::Sa, None),
(102, Layer::Sa, None),
(103, Layer::Oa, Some(Id::from_u128(100))),
(104, Layer::Sa, Some(Id::from_u128(101))),
] {
element(
&mut p,
Id::from_u128(id),
if parent.is_some() {
Kind::DataField
} else {
Kind::DataType
},
layer,
"Record",
parent,
);
}
trace(
&mut p,
Id::from_u128(300),
Id::from_u128(100),
Id::from_u128(101),
);
let facet = match mode {
0 => FacetSelector::Data(DataFacet::RecordSupertype),
1 => FacetSelector::Data(DataFacet::Constructor),
_ => FacetSelector::Child(KeyPair {
oa: Some(Id::from_u128(103)),
sa: Some(Id::from_u128(104)),
}),
};
let path = if mode == 2 {
"/parent"
} else {
"/body/Sa/Shared/DataType/definition/Record/supertype"
};
let mut app = App::with_project(p.clone());
let _ = app.update(Message::StartSemanticReview(Id::from_u128(100)));
if mode == 2 {
// A paired attachment is a real catalog facet only after explicit
// correspondence; selecting an unstaged synthetic pair is rejected.
for (id, oa) in [(103, true), (104, false)] {
let choice = crate::app::Choice {
id: Id::from_u128(id),
label: format!("Record · @object_{id}"),
};
send_review(
&mut app,
if oa {
RowEvent::ReferenceOa(choice)
} else {
RowEvent::ReferenceSa(choice)
},
);
}
send_review(&mut app, RowEvent::AddReference);
}
send_review(&mut app, RowEvent::Select(facet.clone()));
send_review(
&mut app,
RowEvent::Decide(facet.clone(), Some(SemanticDecision::Candidate)),
);
let host = review_host(&app);
let record = if mode == 2 {
Id::from_u128(104)
} else {
Id::from_u128(101)
};
assert_eq!(
app.reference_draft(&host, path)
.unwrap_or_else(|| {
panic!(
"Reference fixture mode={mode}, facet={facet:?}, selected={:?}, decisions={:?}, error={:?}",
review(&app).selected,
review(&app).decisions,
review(&app).error
)
})
.original,
record
);
let before = review(&app).recovery_snapshot().unwrap();
let plan = serde_json::to_value(&review(&app).plan).unwrap();
let proposed = review(&app).proposed.clone();
let source = app.document.bundle().clone();
open(&mut app, &host, path);
search(&mut app, "@object_102 SA");
assert_eq!(
app.reference_authoring
.opened
.as_ref()
.unwrap()
.chooser
.count(),
1
);
assert_eq!(review(&app).recovery_snapshot().unwrap(), before);
assert_eq!(serde_json::to_value(&review(&app).plan).unwrap(), plan);
assert_eq!(review(&app).proposed, proposed);
assert_eq!(app.document.bundle(), &source);
let old = event(&app, Action::Choose(Id::from_u128(102)));
send_review(
&mut app,
RowEvent::Select(FacetSelector::Header(HeaderFacet::Name)),
);
let switched = review(&app).recovery_snapshot().unwrap();
let _ = app.update(old);
assert_eq!(review(&app).recovery_snapshot().unwrap(), switched);
send_review(&mut app, RowEvent::Select(facet));
open(&mut app, &host, path);
choose(&mut app, Id::from_u128(102));
let draft = app.reference_draft(&host, path).unwrap();
let mut expected_record = serde_json::to_value(&p.elements[&record]).unwrap();
*expected_record.pointer_mut(path).unwrap() = json!(Id::from_u128(102));
assert_eq!(draft.materialize().unwrap(), expected_record);
assert_eq!(app.document.bundle(), &source);
assert!(
!app.document.can_undo(),
"A choice only stages the active review candidate"
);
}
}
mod mounted;
#[test]
fn review_reference_cannot_reopen_or_write_a_banked_row_after_another_row_takes_focus() {
let mut p = Project::empty();
for (id, layer) in [
(100, Layer::Oa),
(101, Layer::Sa),
(102, Layer::Sa),
(110, Layer::Oa),
(111, Layer::Sa),
] {
element(
&mut p,
Id::from_u128(id),
Kind::DataType,
layer,
"Record",
None,
);
}
trace(
&mut p,
Id::from_u128(300),
Id::from_u128(100),
Id::from_u128(101),
);
trace(
&mut p,
Id::from_u128(310),
Id::from_u128(110),
Id::from_u128(111),
);
let mut app = App::with_project(p);
let _ = app.update(Message::StartSemanticReview(Id::from_u128(300)));
let facet = FacetSelector::Data(DataFacet::RecordSupertype);
send_review(&mut app, RowEvent::Select(facet.clone()));
send_review(
&mut app,
RowEvent::Decide(facet, Some(SemanticDecision::Candidate)),
);
let host = review_host(&app);
let path = "/body/Sa/Shared/DataType/definition/Record/supertype";
let old_open = request(&app, &host, path);
open(&mut app, &host, path);
let old_choice = event(&app, Action::Choose(Id::from_u128(102)));
for event in [
semantic_review::Event::ChooseTrace(crate::app::Choice {
id: Id::from_u128(310),
label: "Second exact trace".into(),
}),
semantic_review::Event::OpenTrace,
] {
let _ = app.update(Message::Workflow(workflow::Event::semantic(event)));
}
assert_eq!(review(&app).target(app.project()), Some(Id::from_u128(111)));
let expected = review(&app).recovery_snapshot().unwrap();
let source = app.document.bundle().clone();
let _ = app.update(old_choice);
let Host::Review { address, facet } = host else {
unreachable!()
};
let _ = app.update(Message::Workflow(workflow::Event::semantic(
semantic_review::Event::Row {
address,
facet: Some(facet),
event: RowEvent::ReferenceOpen(old_open),
},
)));
assert!(app.reference_authoring.opened.is_none());
assert_eq!(review(&app).recovery_snapshot().unwrap(), expected);
assert_eq!(app.document.bundle(), &source);
assert!(!app.document.can_undo());
}