587 lines
20 KiB
Rust
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());
|
|
}
|