728 lines
25 KiB
Rust
728 lines
25 KiB
Rust
use super::*;
|
|
use crate::document::Document;
|
|
use oa_core::{
|
|
language,
|
|
source_project::{ProjectLimits, SourceBundle},
|
|
validation,
|
|
};
|
|
|
|
fn id(p: &Project, alias: &str) -> Id {
|
|
p.identities()
|
|
.into_iter()
|
|
.find(|(_, name)| *name == alias)
|
|
.unwrap()
|
|
.0
|
|
}
|
|
|
|
fn fixture(lifetime: bool) -> (Document, Id) {
|
|
let (name, input, owner) = if lifetime {
|
|
(
|
|
"lifetime.arcadia",
|
|
include_str!("../behavior-layout-scene-20260912/lifetime.arcadia"),
|
|
"lifetime_conversation",
|
|
)
|
|
} else {
|
|
(
|
|
"fixture.arcadia",
|
|
include_str!("../behavior-layout-scene-20260912/fixture.arcadia"),
|
|
"conversation",
|
|
)
|
|
};
|
|
let p = language::parse(input).expect("existing real Arcadia fixture");
|
|
let errors: Vec<_> = validation::validate(&p)
|
|
.into_iter()
|
|
.filter(|issue| issue.severity == validation::Severity::Error)
|
|
.collect();
|
|
assert!(errors.is_empty(), "{errors:?}");
|
|
let bundle = SourceBundle {
|
|
entry: name.into(),
|
|
files: BTreeMap::from([(name.into(), input.into())]),
|
|
};
|
|
let document = Document::from_bundle(bundle, ProjectLimits::default()).unwrap();
|
|
assert_eq!(document.project(), &p);
|
|
assert_eq!(language::parse(&language::format(&p)).unwrap(), p);
|
|
let owner = id(&p, owner);
|
|
(document, owner)
|
|
}
|
|
|
|
fn passive(document: &Document, before: &Project, source: &SourceBundle) {
|
|
assert_eq!(document.project(), before);
|
|
assert_eq!(document.bundle(), source);
|
|
assert!(!document.dirty());
|
|
assert!(!document.can_undo() && !document.can_redo());
|
|
}
|
|
|
|
fn row(scene: &Scene, id: Id) -> f32 {
|
|
scene.rows[&id].1
|
|
}
|
|
fn line(scene: &Scene, id: Id) -> &Line {
|
|
assert_eq!(scene.lines.iter().filter(|line| line.id == id).count(), 1);
|
|
scene.lines.iter().find(|line| line.id == id).unwrap()
|
|
}
|
|
fn named_box(scene: &Scene, id: Id) -> &BoxItem {
|
|
scene
|
|
.boxes
|
|
.iter()
|
|
.find(|item| item.id == id && !item.title.is_empty())
|
|
.unwrap()
|
|
}
|
|
|
|
fn same_scene(a: &Scene, b: &Scene) {
|
|
assert_eq!(a.rows, b.rows);
|
|
assert_eq!(a.sequence_bounds, b.sequence_bounds);
|
|
assert_eq!(a.size, b.size);
|
|
assert_eq!(a.lanes, b.lanes);
|
|
assert_eq!(a.lifecycle, b.lifecycle);
|
|
assert_eq!(a.open_calls, b.open_calls);
|
|
let boxes = |s: &Scene| {
|
|
s.boxes
|
|
.iter()
|
|
.map(|b| (b.id, b.rect, b.title.clone(), b.detail.clone(), b.marker))
|
|
.collect::<Vec<_>>()
|
|
};
|
|
let lines = |s: &Scene| {
|
|
s.lines
|
|
.iter()
|
|
.map(|l| {
|
|
(
|
|
l.id,
|
|
l.points.clone(),
|
|
l.label_at,
|
|
l.label.clone(),
|
|
l.dashed,
|
|
)
|
|
})
|
|
.collect::<Vec<_>>()
|
|
};
|
|
let slots = |s: &Scene| {
|
|
s.slots
|
|
.iter()
|
|
.map(|slot| {
|
|
(
|
|
slot.slot.parent,
|
|
slot.slot.before,
|
|
slot.y,
|
|
slot.label.clone(),
|
|
)
|
|
})
|
|
.collect::<Vec<_>>()
|
|
};
|
|
assert_eq!(boxes(a), boxes(b));
|
|
assert_eq!(lines(a), lines(b));
|
|
assert_eq!(slots(a), slots(b));
|
|
}
|
|
|
|
fn others_fixed(before: &Scene, after: &Scene, changed: Id) {
|
|
assert_eq!(before.rows.len(), after.rows.len());
|
|
for (&id, original) in &before.rows {
|
|
if id != changed {
|
|
assert_eq!(&after.rows[&id], original, "unexpected movement of {id}");
|
|
}
|
|
}
|
|
}
|
|
|
|
fn fixed_frame_metadata(before: &Scene, after: &Scene, frame: Id) {
|
|
let original = named_box(before, frame);
|
|
let current = named_box(after, frame);
|
|
assert_eq!(
|
|
(
|
|
current.id,
|
|
current.rect,
|
|
¤t.title,
|
|
¤t.detail,
|
|
current.marker
|
|
),
|
|
(
|
|
original.id,
|
|
original.rect,
|
|
&original.title,
|
|
&original.detail,
|
|
original.marker
|
|
)
|
|
);
|
|
assert_eq!(before.rows[&frame], after.rows[&frame]);
|
|
assert_eq!(row(after, frame), 332.);
|
|
// Scene::hit uses the first38px as the frame's active painted header.
|
|
let header = Rectangle::new(
|
|
Point::new(original.rect.x, original.rect.y),
|
|
Size::new(original.rect.width, 38.),
|
|
);
|
|
assert_eq!((header.y, header.y + header.height), (332., 370.));
|
|
let probe = Point::new(header.x + 8., header.y + 8.);
|
|
assert_eq!(before.hit(probe), Some(frame));
|
|
assert_eq!(after.hit(probe), Some(frame));
|
|
let original_slot = before
|
|
.slots
|
|
.iter()
|
|
.find(|slot| slot.slot.before == Some(frame))
|
|
.unwrap();
|
|
let current_slot = after
|
|
.slots
|
|
.iter()
|
|
.find(|slot| slot.slot.before == Some(frame))
|
|
.unwrap();
|
|
assert_eq!(
|
|
(
|
|
current_slot.slot.parent,
|
|
current_slot.slot.before,
|
|
current_slot.y,
|
|
¤t_slot.label
|
|
),
|
|
(
|
|
original_slot.slot.parent,
|
|
original_slot.slot.before,
|
|
original_slot.y,
|
|
&original_slot.label
|
|
)
|
|
);
|
|
assert_eq!(current_slot.y, 290.);
|
|
}
|
|
|
|
fn actual_lifetime_projection(
|
|
p: &Project,
|
|
owner: Id,
|
|
scene: &Scene,
|
|
) -> Vec<scenario_lifecycle::LaneLifetime> {
|
|
// Same actual rows/scope inputs as Rendering::new, including its root span
|
|
// and empty-operand allowance; call the real projector for diagnostics.
|
|
let mut scopes = scene.sequence_bounds.clone();
|
|
for (&scope, (start, end)) in &mut scopes {
|
|
if scope != owner && *start == *end {
|
|
*end = *start + 26.;
|
|
}
|
|
}
|
|
scopes.insert(owner, (85., scene.size.height - 20.));
|
|
scenario_lifecycle::project_lifetimes(
|
|
p,
|
|
owner,
|
|
&scenario_lifecycle::Geometry {
|
|
rows: &scene.rows,
|
|
scopes: &scopes,
|
|
},
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn complete_seed_plan_preserves_both_actual_scene_and_document_baselines() {
|
|
for lifetime in [false, true] {
|
|
let (document, owner) = fixture(lifetime);
|
|
let p = document.project();
|
|
let before = p.clone();
|
|
let source = document.bundle().clone();
|
|
let baseline = Scene::sequence(p, owner, owner);
|
|
let plan = Plan::build(p, owner, &baseline, &Candidate::new()).unwrap();
|
|
assert!(plan.diagnostics.is_empty());
|
|
for (&id, (_, y)) in &baseline.rows {
|
|
assert_eq!(plan.anchors[&id].y, *y);
|
|
}
|
|
same_scene(&baseline, &render(p, owner, &plan));
|
|
let reparsed = language::parse(&language::format(p)).unwrap();
|
|
same_scene(&baseline, &render(&reparsed, owner, &plan));
|
|
passive(&document, &before, &source);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn upward_second_delivery_reaches_200_and_all_other_occurrence_anchors_stay_fixed() {
|
|
let (document, owner) = fixture(false);
|
|
let p = document.project();
|
|
let before = p.clone();
|
|
let source = document.bundle().clone();
|
|
let baseline = Scene::sequence(p, owner, owner);
|
|
let first = id(p, "first");
|
|
let second = id(p, "second");
|
|
assert_ne!(first, second);
|
|
for occurrence_id in [first, second] {
|
|
assert!(
|
|
matches!(occurrence(p,occurrence_id),Some(OccurrenceBody::Exchange { exchange:Some(edge),.. }) if *edge==id(p,"channel"))
|
|
);
|
|
}
|
|
assert_eq!(
|
|
(row(&baseline, first), row(&baseline, second)),
|
|
(148., 240.)
|
|
);
|
|
let candidate = drag(&Candidate::new(), &baseline, second, -40.);
|
|
assert_eq!(candidate, Candidate::from([(second, 200.)]));
|
|
let plan = Plan::build(p, owner, &baseline, &candidate).unwrap();
|
|
let moved = render(p, owner, &plan);
|
|
assert_eq!((row(&moved, first), row(&moved, second)), (148., 200.));
|
|
others_fixed(&baseline, &moved, second);
|
|
assert_eq!(line(&moved, first).points, line(&baseline, first).points);
|
|
let left = moved.lanes[&id(p, "left")];
|
|
let right = moved.lanes[&id(p, "right")];
|
|
assert_eq!(
|
|
line(&moved, second).points,
|
|
vec![Point::new(left, 200.), Point::new(right, 200.)]
|
|
);
|
|
assert_eq!(
|
|
plan.messages[&second],
|
|
MessageAnchors {
|
|
send_y: 200.,
|
|
receive_y: 200.
|
|
}
|
|
);
|
|
assert!(plan.diagnostics.is_empty());
|
|
same_scene(&moved, &render(p, owner, &plan));
|
|
passive(&document, &before, &source);
|
|
}
|
|
|
|
#[test]
|
|
fn nested_upward_anchor_reaches_457_without_moving_parent_or_followers() {
|
|
let (document, owner) = fixture(false);
|
|
let p = document.project();
|
|
let before = p.clone();
|
|
let source = document.bundle().clone();
|
|
let baseline = Scene::sequence(p, owner, owner);
|
|
let nested = id(p, "nested");
|
|
let fragment = id(p, "fragment");
|
|
assert_eq!(row(&baseline, nested), 477.);
|
|
let candidate = drag(&Candidate::new(), &baseline, nested, -20.);
|
|
let plan = Plan::build(p, owner, &baseline, &candidate).unwrap();
|
|
let moved = render(p, owner, &plan);
|
|
assert_eq!(row(&moved, nested), 457.);
|
|
assert_eq!(named_box(&moved, nested).rect.y, 437.);
|
|
assert_eq!(named_box(&moved, nested).rect.height, 70.);
|
|
assert_eq!(
|
|
named_box(&moved, fragment).rect,
|
|
named_box(&baseline, fragment).rect
|
|
);
|
|
assert_eq!(
|
|
(
|
|
named_box(&moved, fragment).rect.y,
|
|
named_box(&moved, fragment).rect.height
|
|
),
|
|
(332., 240.)
|
|
);
|
|
fixed_frame_metadata(&baseline, &moved, fragment);
|
|
assert_eq!(
|
|
moved.sequence_bounds[&id(p, "operand")],
|
|
baseline.sequence_bounds[&id(p, "operand")]
|
|
);
|
|
others_fixed(&baseline, &moved, nested);
|
|
assert!(plan.diagnostics.is_empty());
|
|
passive(&document, &before, &source);
|
|
}
|
|
|
|
#[test]
|
|
fn nested_move_that_would_relocate_fixed_header_is_rejected_but_mere_overlap_is_not_banned() {
|
|
let (document, owner) = fixture(false);
|
|
let p = document.project();
|
|
let before = p.clone();
|
|
let source = document.bundle().clone();
|
|
let baseline = Scene::sequence(p, owner, owner);
|
|
let nested = id(p, "nested");
|
|
let fragment = id(p, "fragment");
|
|
let requested = Candidate::from([(nested, 300.)]);
|
|
// Actual70px action box has row-to-top offset-20: proposed footprint280..350.
|
|
// The old envelope union would move frame/header top332 to260 while leaving
|
|
// FrameStart332 and before-slot290. Reject before any such Scene is exposed.
|
|
let original = named_box(&baseline, nested).rect;
|
|
assert_eq!(
|
|
(row(&baseline, nested), original.y, original.height),
|
|
(477., 457., 70.)
|
|
);
|
|
let proposed_top = original.y + (300. - row(&baseline, nested));
|
|
assert_eq!((proposed_top, proposed_top + original.height), (280., 350.));
|
|
assert_eq!(
|
|
Plan::build(p, owner, &baseline, &requested).unwrap_err(),
|
|
PlanError::FixedFrameHeaderWouldMove { frame: fragment }
|
|
);
|
|
fixed_frame_metadata(&baseline, &Scene::sequence(p, owner, owner), fragment);
|
|
// Overlap inside the existing frame does not itself move its top. This
|
|
// remains admitted mechanical geometry, not a claim of usable overlap UX.
|
|
let overlap = Plan::build(p, owner, &baseline, &Candidate::from([(nested, 380.)])).unwrap();
|
|
let overlapping = render(p, owner, &overlap);
|
|
assert_eq!(named_box(&overlapping, nested).rect.y, 360.);
|
|
assert_eq!(overlapping.sequence_bounds[&id(p, "operand")], (338., 530.));
|
|
fixed_frame_metadata(&baseline, &overlapping, fragment);
|
|
let original_lifetimes = actual_lifetime_projection(p, owner, &baseline);
|
|
let moved_lifetimes = actual_lifetime_projection(p, owner, &overlapping);
|
|
assert_eq!(original_lifetimes.len(), 2);
|
|
assert_eq!(moved_lifetimes.len(), 2);
|
|
assert!(
|
|
original_lifetimes
|
|
.iter()
|
|
.all(|lane| lane.uncertainties.is_empty())
|
|
);
|
|
assert!(
|
|
moved_lifetimes
|
|
.iter()
|
|
.all(|lane| lane.uncertainties.is_empty()),
|
|
"{moved_lifetimes:?}"
|
|
);
|
|
let receiver = id(p, "right");
|
|
for scene in [&baseline, &overlapping] {
|
|
assert_eq!(
|
|
scene
|
|
.lifecycle
|
|
.alive_spans(receiver, owner, 530., scene.size.height - 20.)
|
|
.iter()
|
|
.map(|span| (span.start, span.end))
|
|
.collect::<Vec<_>>(),
|
|
vec![(530., 845.)]
|
|
);
|
|
}
|
|
let call = id(p, "self_call");
|
|
let bars = |scene: &Scene| {
|
|
scene
|
|
.boxes
|
|
.iter()
|
|
.filter(|item| item.id == call && item.title.is_empty())
|
|
.map(|item| (item.rect, item.marker))
|
|
.collect::<Vec<_>>()
|
|
};
|
|
assert!(!bars(&baseline).is_empty());
|
|
assert_eq!(bars(&overlapping), bars(&baseline));
|
|
passive(&document, &before, &source);
|
|
}
|
|
|
|
#[test]
|
|
fn nested353_scope_escape_is_named_and_rejected_before_missing_lifecycle_geometry_is_exposed() {
|
|
let (document, owner) = fixture(false);
|
|
let p = document.project();
|
|
let before = p.clone();
|
|
let source = document.bundle().clone();
|
|
let baseline = Scene::sequence(p, owner, owner);
|
|
let nested = id(p, "nested");
|
|
let operand = id(p, "operand");
|
|
let fragment = id(p, "fragment");
|
|
let original = named_box(&baseline, nested).rect;
|
|
assert_eq!(
|
|
(original.y, baseline.sequence_bounds[&operand].0),
|
|
(457., 435.)
|
|
);
|
|
let proposed_top = original.y + (353. - row(&baseline, nested));
|
|
let proposed_scope_start = proposed_top - (original.y - baseline.sequence_bounds[&operand].0);
|
|
assert_eq!(
|
|
(proposed_top, proposed_scope_start, row(&baseline, fragment)),
|
|
(333., 311., 332.)
|
|
);
|
|
assert_eq!(
|
|
Plan::build(p, owner, &baseline, &Candidate::from([(nested, 353.)])).unwrap_err(),
|
|
PlanError::OperandScopeOutsideEnclosingInterval { operand, fragment }
|
|
);
|
|
same_scene(&baseline, &Scene::sequence(p, owner, owner));
|
|
passive(&document, &before, &source);
|
|
}
|
|
|
|
#[test]
|
|
fn lifecycle_anchor_below_actual_root_render_scope_is_rejected_before_scene_construction() {
|
|
let (document, owner) = fixture(true);
|
|
let p = document.project();
|
|
let before = p.clone();
|
|
let source = document.bundle().clone();
|
|
let baseline = Scene::sequence(p, owner, owner);
|
|
let create = id(p, "create_receiver");
|
|
let provider = id(p, "provider");
|
|
let requester = id(p, "requester");
|
|
let root_life = baseline
|
|
.lifecycle
|
|
.alive_spans(requester, owner, 0., baseline.size.height);
|
|
assert_eq!(
|
|
root_life
|
|
.iter()
|
|
.map(|span| (span.start, span.end))
|
|
.collect::<Vec<_>>(),
|
|
vec![(85., 544.)]
|
|
);
|
|
for requested in [50., 84.] {
|
|
assert_eq!(
|
|
Plan::build(p, owner, &baseline, &Candidate::from([(create, requested)])).unwrap_err(),
|
|
PlanError::LifecycleAnchorOutsideRenderScope {
|
|
occurrence: create,
|
|
scope: owner
|
|
}
|
|
);
|
|
}
|
|
// At the renderer's actual85 boundary the marker/lifetime remains present.
|
|
// Source-order crossing is still diagnosed separately, not silently banned.
|
|
let boundary = Plan::build(p, owner, &baseline, &Candidate::from([(create, 85.)])).unwrap();
|
|
let moved = render(p, owner, &boundary);
|
|
let x = moved.lanes[&provider];
|
|
assert_eq!(row(&moved, create), 85.);
|
|
assert_eq!(moved.lifecycle.marker_at(Point::new(x, 85.)), Some(create));
|
|
let alive = moved
|
|
.lifecycle
|
|
.alive_spans(provider, owner, 0., moved.size.height);
|
|
assert_eq!(
|
|
alive
|
|
.iter()
|
|
.map(|span| (span.start, span.end))
|
|
.collect::<Vec<_>>(),
|
|
vec![(85., 427.)]
|
|
);
|
|
passive(&document, &before, &source);
|
|
}
|
|
|
|
#[test]
|
|
fn earlier_death_reaches_380_and_actual_lifetime_and_activation_clip_there() {
|
|
let (document, owner) = fixture(true);
|
|
let p = document.project();
|
|
let before = p.clone();
|
|
let source = document.bundle().clone();
|
|
let baseline = Scene::sequence(p, owner, owner);
|
|
let create = id(p, "create_receiver");
|
|
let call = id(p, "service_call");
|
|
let delete = id(p, "delete_receiver");
|
|
let provider = id(p, "provider");
|
|
assert_eq!(
|
|
(
|
|
row(&baseline, create),
|
|
row(&baseline, call),
|
|
row(&baseline, delete)
|
|
),
|
|
(243., 335., 427.)
|
|
);
|
|
let candidate = drag(&Candidate::new(), &baseline, delete, -47.);
|
|
let plan = Plan::build(p, owner, &baseline, &candidate).unwrap();
|
|
assert_eq!(plan.anchors[&delete].role, AnchorRole::MessageReceive);
|
|
assert_eq!(
|
|
plan.messages[&delete],
|
|
MessageAnchors {
|
|
send_y: 380.,
|
|
receive_y: 380.
|
|
}
|
|
);
|
|
let moved = render(p, owner, &plan);
|
|
assert_eq!(row(&moved, delete), 380.);
|
|
others_fixed(&baseline, &moved, delete);
|
|
let spans = moved
|
|
.lifecycle
|
|
.alive_spans(provider, owner, 85., moved.size.height - 20.);
|
|
assert_eq!(
|
|
spans.iter().map(|s| (s.start, s.end)).collect::<Vec<_>>(),
|
|
vec![(243., 380.)]
|
|
);
|
|
assert!(
|
|
moved
|
|
.lifecycle
|
|
.alive_spans(provider, owner, 380., moved.size.height - 20.)
|
|
.is_empty()
|
|
);
|
|
let x = moved.lanes[&provider];
|
|
assert_eq!(moved.lifecycle.marker_at(Point::new(x, 380.)), Some(delete));
|
|
assert_eq!(moved.lifecycle.marker_at(Point::new(x, 427.)), None);
|
|
assert_eq!(moved.hit(Point::new(x, 380.)), Some(delete));
|
|
let bars: Vec<_> = moved
|
|
.boxes
|
|
.iter()
|
|
.filter(|b| b.id == call && b.title.is_empty())
|
|
.collect();
|
|
assert_eq!(bars.len(), 1);
|
|
assert_eq!(
|
|
bars[0].rect,
|
|
Rectangle::new(Point::new(x - 4., 335.), Size::new(8., 45.))
|
|
);
|
|
assert_eq!(moved.open_calls, vec![Point::new(x + 12., 364.)]);
|
|
assert!(plan.diagnostics.is_empty());
|
|
passive(&document, &before, &source);
|
|
}
|
|
|
|
#[test]
|
|
fn self_call_has_explicit_send610_receive634_and_reply_stays728() {
|
|
let (document, owner) = fixture(false);
|
|
let p = document.project();
|
|
let before = p.clone();
|
|
let source = document.bundle().clone();
|
|
let baseline = Scene::sequence(p, owner, owner);
|
|
let call = id(p, "self_call");
|
|
let reply = id(p, "self_reply");
|
|
let candidate = drag(&Candidate::new(), &baseline, call, -26.);
|
|
let plan = Plan::build(p, owner, &baseline, &candidate).unwrap();
|
|
assert_eq!(
|
|
plan.anchors[&call],
|
|
Anchor {
|
|
y: 610.,
|
|
role: AnchorRole::MessageSend
|
|
}
|
|
);
|
|
assert_eq!(
|
|
plan.messages[&call],
|
|
MessageAnchors {
|
|
send_y: 610.,
|
|
receive_y: 634.
|
|
}
|
|
);
|
|
let moved = render(p, owner, &plan);
|
|
assert_eq!(row(&moved, call), 610.);
|
|
assert_eq!(row(&moved, reply), 728.);
|
|
assert_eq!(
|
|
line(&moved, call)
|
|
.points
|
|
.iter()
|
|
.map(|p| p.y)
|
|
.collect::<Vec<_>>(),
|
|
vec![610., 610., 634., 634.]
|
|
);
|
|
assert_eq!(line(&moved, reply).points, line(&baseline, reply).points);
|
|
others_fixed(&baseline, &moved, call);
|
|
// Peer #21 owns the activation start/column contract. This assertion covers
|
|
// actual message anchors, not the old row-based activation start defect.
|
|
passive(&document, &before, &source);
|
|
}
|
|
|
|
#[test]
|
|
fn signed_zero_keeps_sparse_candidate_and_restoring_original_candidate_restores_actual_scene() {
|
|
let (document, owner) = fixture(false);
|
|
let p = document.project();
|
|
let before = p.clone();
|
|
let source = document.bundle().clone();
|
|
let baseline = Scene::sequence(p, owner, owner);
|
|
let second = id(p, "second");
|
|
let original = Candidate::new();
|
|
for zero in [0., -0.] {
|
|
assert_eq!(drag(&original, &baseline, second, zero), original);
|
|
}
|
|
let changed = drag(&original, &baseline, second, -40.);
|
|
let plan = Plan::build(p, owner, &baseline, &changed).unwrap();
|
|
let frame = render(p, owner, &plan);
|
|
for zero in [0., -0.] {
|
|
let untouched = drag(&changed, &frame, second, zero);
|
|
assert_eq!(untouched, changed);
|
|
same_scene(
|
|
&frame,
|
|
&render(
|
|
p,
|
|
owner,
|
|
&Plan::build(p, owner, &baseline, &untouched).unwrap(),
|
|
),
|
|
);
|
|
}
|
|
// This restores the exact sidecar candidate; it is not a production Undo
|
|
// command or evidence of a layout source/REST persistence implementation.
|
|
same_scene(
|
|
&baseline,
|
|
&render(
|
|
p,
|
|
owner,
|
|
&Plan::build(p, owner, &baseline, &original).unwrap(),
|
|
),
|
|
);
|
|
passive(&document, &before, &source);
|
|
}
|
|
|
|
#[test]
|
|
fn crossing_is_rendered_exactly_and_diagnosed_with_extents_covering_the_earlier_far_event() {
|
|
let (document, owner) = fixture(false);
|
|
let p = document.project();
|
|
let before = p.clone();
|
|
let source = document.bundle().clone();
|
|
let baseline = Scene::sequence(p, owner, owner);
|
|
let first = id(p, "first");
|
|
let second = id(p, "second");
|
|
let plan = Plan::build(p, owner, &baseline, &Candidate::from([(first, 1000.)])).unwrap();
|
|
assert!(plan.diagnostics.contains(&Diagnostic::SourceOrderCrossing {
|
|
scope: owner,
|
|
before: first,
|
|
after: second
|
|
}));
|
|
let moved = render(p, owner, &plan);
|
|
assert_eq!((row(&moved, first), row(&moved, second)), (1000., 240.));
|
|
assert_eq!(row(&moved, id(p, "self_reply")), 728.);
|
|
others_fixed(&baseline, &moved, first);
|
|
assert_eq!(moved.sequence_bounds[&owner].1, 1026.);
|
|
assert_eq!(moved.size.height, 1113.);
|
|
assert!(
|
|
line(&moved, first)
|
|
.points
|
|
.iter()
|
|
.all(|point| point.y < moved.size.height)
|
|
);
|
|
assert_eq!(&p.elements[&owner].steps().unwrap()[..2], &[first, second]);
|
|
// This is a policy falsifier: mechanically exact coordinates do not prove
|
|
// that out-of-order vertical notation is understandable or acceptable.
|
|
passive(&document, &before, &source);
|
|
}
|
|
|
|
#[test]
|
|
fn nested_outlier_expands_enclosing_geometry_without_using_final_traversal_row_as_extent() {
|
|
let (document, owner) = fixture(false);
|
|
let p = document.project();
|
|
let baseline = Scene::sequence(p, owner, owner);
|
|
let nested = id(p, "nested");
|
|
let fragment = id(p, "fragment");
|
|
let following = id(p, "self_call");
|
|
let plan = Plan::build(p, owner, &baseline, &Candidate::from([(nested, 1000.)])).unwrap();
|
|
assert!(
|
|
plan.diagnostics
|
|
.contains(&Diagnostic::FrameExtendsPastFollowingAnchor {
|
|
frame: fragment,
|
|
following
|
|
})
|
|
);
|
|
let moved = render(p, owner, &plan);
|
|
assert_eq!(row(&moved, nested), 1000.);
|
|
assert_eq!(named_box(&moved, nested).rect.y, 980.);
|
|
let enclosing = named_box(&moved, fragment).rect;
|
|
assert_eq!(enclosing.y + enclosing.height, 1070.);
|
|
assert!(moved.sequence_bounds[&owner].1 >= 1070.);
|
|
assert!(moved.size.height > 1070.);
|
|
assert_eq!(row(&moved, following), 636.);
|
|
others_fixed(&baseline, &moved, nested);
|
|
}
|
|
|
|
#[test]
|
|
fn unsupported_targets_and_lifetime_inversion_fail_explicitly_instead_of_silent_clamping() {
|
|
let (document, owner) = fixture(true);
|
|
let p = document.project();
|
|
let baseline = Scene::sequence(p, owner, owner);
|
|
let create = id(p, "create_receiver");
|
|
let delete = id(p, "delete_receiver");
|
|
assert_eq!(
|
|
Plan::build(p, owner, &baseline, &Candidate::from([(delete, 200.)])).unwrap_err(),
|
|
PlanError::LifetimeInversionNotSupported { create, delete }
|
|
);
|
|
assert_eq!(
|
|
Plan::build(p, owner, &baseline, &Candidate::from([(delete, f32::NAN)])).unwrap_err(),
|
|
PlanError::NonFinite(delete)
|
|
);
|
|
let edge = id(p, "service");
|
|
assert_eq!(
|
|
Plan::build(p, owner, &baseline, &Candidate::from([(edge, 200.)])).unwrap_err(),
|
|
PlanError::UnknownAnchor(edge)
|
|
);
|
|
let (other, other_owner) = fixture(false);
|
|
let fragment = id(other.project(), "fragment");
|
|
let other_scene = Scene::sequence(other.project(), other_owner, other_owner);
|
|
assert_eq!(
|
|
Plan::build(
|
|
other.project(),
|
|
other_owner,
|
|
&other_scene,
|
|
&Candidate::from([(fragment, 300.)])
|
|
)
|
|
.unwrap_err(),
|
|
PlanError::CompoundMoveNotImplemented(fragment)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn complete_plan_scope_is_inactive_in_other_threads_and_restored_after_deliberate_panic() {
|
|
let (document, owner) = fixture(false);
|
|
let p = document.project();
|
|
let baseline = Scene::sequence(p, owner, owner);
|
|
let second = id(p, "second");
|
|
let plan = Plan::build(p, owner, &baseline, &Candidate::from([(second, 200.)])).unwrap();
|
|
let caught = std::panic::catch_unwind(|| {
|
|
let _scope = Scope::install(&plan);
|
|
assert_eq!(row(&Scene::sequence(p, owner, owner), second), 200.);
|
|
std::thread::spawn(|| {
|
|
let (document, owner) = fixture(false);
|
|
let p = document.project();
|
|
assert_eq!(
|
|
row(&Scene::sequence(p, owner, owner), id(p, "second")),
|
|
240.
|
|
);
|
|
})
|
|
.join()
|
|
.unwrap();
|
|
panic!("deliberate complete-anchor scope unwind");
|
|
});
|
|
let payload = caught.expect_err("deliberate panic is required");
|
|
assert_eq!(
|
|
payload.downcast_ref::<&str>().copied(),
|
|
Some("deliberate complete-anchor scope unwind")
|
|
);
|
|
same_scene(&baseline, &Scene::sequence(p, owner, owner));
|
|
}
|