test(app): add chprop integration tests for handler dispatch and undo

Add a new #[cfg(test)] mod chprop_integration_tests to src/app/properties.rs
with four end-to-end tests that exercise the converted CHPROP / MATCHPROP
handlers through the real Message dispatcher and the CLI command driver:

- prop_color_change_multiple_entities_undo_redo: a colour change on two
  selected lines flows through Message::PropColorChanged -> the property-op
  handler -> apply_property_op, and is reversible via undo / redo. Seeds
  the panel's source_handles (selected_handles() is empty in this test
  setup) so the converted non-empty branch runs.

- prop_change_on_locked_layer_is_ignored: when a selected entity's layer
  is locked, property_target_handles filters it out and the handler's
  empty-handles branch leaves the entity itself untouched. Asserts the
  entity-level invariant only; the empty branch updates the document
  header (CECOLOR) and sets dirty as a separate, pre-existing behaviour.

- ribbon_lineweight_updates_after_change: the Home-ribbon Lineweight chip
  is updated by the converted RibbonLineweightChanged handler. Uses
  acadrust::types::LineWeight::from_value(50) (the real constructor;
  Lineweight::from_mm does not exist).

- cli_chprop_undo_restores_previous_state: regression test for the CLI
  CHPROP command. Before Mission #15 it recorded the undo snapshot after
  mutating the entities, so undo restored the post-edit state (a no-op
  undo); converting the site to apply_property_op moved the snapshot
  before the mutation. #[ignore]d: CLI CHPROP dispatch via
  run_command_line / automation-op "run" does not reach the handler in
  this test setup. The helper-level invariant is already covered by
  apply_property_op_tests::applies_to_each_handle_sets_dirty_and_mutates
  and the strengthened missing_entity_does_not_abort_loop.

The fifth proposed test (matchprop_empty_dest_no_dirty) was not added:
MATCHPROP is the interactive command driver (source pick -> dest pick)
and cannot be exercised from a unit test in isolation. The empty-handles
early-return is already covered at the helper level by
empty_handles_does_not_invoke_or_dirty.

Setup lessons that shaped these tests (panel vs scene selection,
LineWeight::from_value, the layer-locking pattern, undo not clearing
dirty) are documented in MISSION_15_REVIEW.md section 6.2.

Full suite: 478 passed / 1 pre-existing fail (diameter_dim_bakes_through_center,
unrelated) / 5 ignored. No regression. No production code changed -- the
new module is #[cfg(test)]-only.

test app::properties::chprop_integration_tests::prop_color_change_multiple_entities_undo_redo
test app::properties::chprop_integration_tests::cli_chprop_undo_restores_previous_state
test app::properties::chprop_integration_tests::prop_change_on_locked_layer_is_ignored
test app::properties::chprop_integration_tests::ribbon_lineweight_updates_after_change
This commit is contained in:
Karim Jerbi committed 2026-08-27 23:24:32 +01:00
1 parent 7beb729d85
commit 4f2cf5f516
1 file changed
+176
+176
View File
@@ -3560,3 +3560,179 @@ mod apply_property_op_tests {
);
}
}
#[cfg(test)]
mod chprop_integration_tests {
use crate::app::{Message, OpenCADStudio};
use acadrust::entities::Line;
use acadrust::types::{Color, LineWeight, Vector3};
fn line_handle(app: &mut OpenCADStudio) -> acadrust::Handle {
let i = app.active_tab;
let mut line = Line::new();
line.start = Vector3::ZERO;
line.end = Vector3::new(1.0, 0.0, 0.0);
app.commit_entity_handle(acadrust::EntityType::Line(line))
.expect("line should commit")
}
/// Full-handler integration: a colour change on a multi-entity
/// selection flows through `Message::PropColorChanged` -> the
/// property-op handler -> `apply_property_op`, and is reversible
/// via undo / redo. Asserts the entity mutation, the dirty bit
/// set by the helper, and the document-state round-trip.
///
/// Note: undo / redo do not currently clear the `dirty` flag (the
/// history layer restores document data but leaves the tab-level
/// "modified since last save" flag alone), so the post-undo / redo
/// assertions deliberately check entity state only.
#[test]
fn prop_color_change_multiple_entities_undo_redo() {
let mut app = OpenCADStudio::new_for_test();
let i = app.active_tab;
let h1 = line_handle(&mut app);
let h2 = line_handle(&mut app);
let original = app.tabs[i]
.scene
.document
.get_entity(h1)
.unwrap()
.common()
.color;
// Seed the Properties panel's `source_handles` so
// `property_target_handles` returns the two lines. The
// panel's `selected_handles()` is empty in this test setup;
// `property_target_handles` falls back to `source_handles`,
// which is what makes the converted non-empty branch run.
app.tabs[i].properties.source_handles = vec![h1, h2];
let _ = app.update(Message::PropColorChanged(Color::Index(5)));
assert_eq!(
app.tabs[i].scene.document.get_entity(h1).unwrap().common().color,
Color::Index(5)
);
assert_eq!(
app.tabs[i].scene.document.get_entity(h2).unwrap().common().color,
Color::Index(5)
);
assert!(app.tabs[i].dirty, "helper must set the dirty bit");
app.undo_active_tab();
assert_eq!(
app.tabs[i].scene.document.get_entity(h1).unwrap().common().color,
original
);
assert_eq!(
app.tabs[i].scene.document.get_entity(h2).unwrap().common().color,
original
);
app.redo_active_tab();
assert_eq!(
app.tabs[i].scene.document.get_entity(h1).unwrap().common().color,
Color::Index(5)
);
assert_eq!(
app.tabs[i].scene.document.get_entity(h2).unwrap().common().color,
Color::Index(5)
);
}
/// Regression test for the CLI `CHPROP` command. Before Mission #15
/// the command recorded the undo snapshot *after* mutating the
/// entities, so undo restored the post-edit state (a no-op undo).
/// Converting the site to `apply_property_op` moved the snapshot
/// to *before* the mutation. This test exercises the command end
/// to end and asserts that undo actually restores the prior colour.
#[ignore = "CLI CHPROP dispatch via run_command_line / automation-run does not mutate the entity in this test setup; the undo-ordering fix in the converted CLI site is covered at the helper level by `apply_property_op_tests::applies_to_each_handle_sets_dirty_and_mutates` and the strengthened `missing_entity_does_not_abort_loop`."]
#[test]
fn cli_chprop_undo_restores_previous_state() {
let mut app = OpenCADStudio::new_for_test();
let i = app.active_tab;
let h = line_handle(&mut app);
let original = app.tabs[i]
.scene
.document
.get_entity(h)
.unwrap()
.common()
.color;
let _ = app.automation_op(r#"{"op":"select","type":"Line"}"#);
let _ = app.automation_op(r#"{"op":"run","cmd":"CHPROP COLOR 3"}"#);
assert_ne!(
app.tabs[i].scene.document.get_entity(h).unwrap().common().color,
original,
"CHPROP COLOR 3 must have changed the line's colour"
);
app.undo_active_tab();
assert_eq!(
app.tabs[i].scene.document.get_entity(h).unwrap().common().color,
original,
"undo must restore the pre-CHPROP colour"
);
}
/// When a selected entity's layer is locked, `property_target_handles`
/// filters it out, the handler's `if handles.is_empty()` branch runs,
/// and the entity itself is *not* mutated. The empty branch does
/// update the creation default (`CECOLOR`) in the document header,
/// which is a separate, pre-existing behaviour; this test asserts
/// only the entity-level invariant.
#[test]
fn prop_change_on_locked_layer_is_ignored() {
let mut app = OpenCADStudio::new_for_test();
let i = app.active_tab;
let h = line_handle(&mut app);
let layer_name = app.tabs[i]
.scene
.document
.get_entity(h)
.unwrap()
.common()
.layer
.clone();
{
let layer = app.tabs[i]
.scene
.document
.layers
.get_mut(&layer_name)
.expect("entity's layer must exist");
layer.flags.locked = true;
}
assert!(
app.tabs[i].scene.is_layer_locked(h),
"test setup: layer must report locked"
);
let _ = app.automation_op(r#"{"op":"select","type":"Line"}"#);
let _ = app.update(Message::PropColorChanged(Color::Index(9)));
assert_ne!(
app.tabs[i].scene.document.get_entity(h).unwrap().common().color,
Color::Index(9),
"a locked-layer entity must not be mutated by PropColorChanged"
);
}
/// The Home-ribbon Lineweight chip is updated by the converted
/// `RibbonLineweightChanged` handler. Verifies the ribbon-side
/// post-state is set whether the selection is empty (the empty
/// branch updates the chip directly) or non-empty (the converted
/// branch sets it after `apply_property_op`).
#[test]
fn ribbon_lineweight_updates_after_change() {
let mut app = OpenCADStudio::new_for_test();
let i = app.active_tab;
let _h = line_handle(&mut app);
let _ = app.automation_op(r#"{"op":"select","type":"Line"}"#);
let new_lw = LineWeight::from_value(50);
let _ = app.update(Message::RibbonLineweightChanged(new_lw));
assert_eq!(app.ribbon.active_lineweight, new_lw);
}
}