Files
OneCAD-Draft/src/app/dock/tests.rs
T
Claude 2ff93f479b fix(ui): open drop_down_below lists right under their field in pallets
iced_aw's DropDown keeps its list on screen by testing it against the
viewport's width and height only, as if the viewport started at the
window corner. Inside a scrollable the viewport is the visible area in
window coordinates, so a field in a dock pallet saw its list overflow
and the list jumped left by its own width (or up over the field); the
Properties Transparency dropdown showed it. Hand the drop-down a
viewport reaching from the corner to the visible area's far edges.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_014ihuqzxdUhd9UbioPn5MGu
2026-10-15 19:09:51 +00:00

1401 lines
54 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Dock behaviour through the app: drags, hover, menus, and the real view.
use crate::app::{Message, OpenCADStudio};
fn fresh() -> OpenCADStudio {
let mut app = OpenCADStudio::new_for_test();
app.automation_op(r#"{"op":"new"}"#);
app
}
#[test]
fn blockpalette_pin_toggles_autocollapse_and_close_hides() {
let mut app = fresh();
app.show_block_palette = true;
let id = crate::ui::dock::PanelId::BlockPalette;
let _ = app.on_dock(crate::ui::dock::DockMsg::AutoCollapseToggle(id));
assert!(app.dock.auto_hides(id), "pin enables auto-hide");
let _ = app.on_dock(crate::ui::dock::DockMsg::AutoCollapseToggle(id));
assert!(!app.dock.auto_hides(id), "second pin disables auto-hide");
let _ = app.on_dock(crate::ui::dock::DockMsg::Close(id));
assert!(!app.show_block_palette, "close dismisses the sidebar");
}
/// A hermetic app with a 1600×900 workspace and the default dock layout
/// (Properties left, block palette right), both shown.
/// Run `f` on a thread with the stack the application's own threads get:
/// building and driving the real view is deeper than a default test thread.
/// One at a time: two headless renderers made in parallel crash the process.
fn with_stack(f: impl FnOnce() + Send + 'static) {
static REAL_VIEW: std::sync::Mutex<()> = std::sync::Mutex::new(());
let _one = REAL_VIEW
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
std::thread::Builder::new()
.stack_size(32 * 1024 * 1024)
.spawn(f)
.expect("spawn test thread")
.join()
.unwrap_or_else(|e| std::panic::resume_unwind(e));
}
fn dock_app() -> OpenCADStudio {
let mut app = fresh();
// Tests load the user's persisted config; reset the dock to a known
// state so this stays hermetic.
app.dock = Default::default();
app.dock.ensure_settings();
app.show_properties = true;
app.show_block_palette = true;
let i = app.active_tab;
app.tabs[i].scene.selection.borrow_mut().view.vp_size = (1600.0, 900.0);
app.win_size = (1600.0, 900.0).into();
app
}
/// Where the current dock drag would land, if it has started.
fn drag_target(app: &OpenCADStudio) -> Option<crate::ui::dock::DropTarget> {
app.dock_drag
.and_then(|d| d.movement())
.and_then(|(_, _, target)| target)
}
/// Press `id`'s title bar at `from`, drag to `to`, and return the target
/// shown just before release.
fn drag_panel(
app: &mut OpenCADStudio,
id: crate::ui::dock::PanelId,
from: iced::Point,
to: iced::Point,
) -> Option<crate::ui::dock::DropTarget> {
use crate::ui::dock::DockMsg;
let _ = app.on_dock(DockMsg::DockGrab(id));
let _ = app.on_dock(DockMsg::DragMove(from));
let _ = app.on_dock(DockMsg::DragMove(to));
let target = drag_target(app);
let _ = app.on_dock(DockMsg::DragRelease);
target
}
#[test]
fn dropping_on_a_pallets_top_or_bottom_half_stacks_above_or_below_it() {
use crate::app::config::DockSide;
use crate::ui::dock::{DropTarget, PanelId};
let mut app = dock_app();
let id = PanelId::BlockPalette;
assert_eq!(app.dock.location(id), Some((DockSide::Right, 0)));
let target = drag_panel(
&mut app,
id,
iced::Point::new(1500.0, 10.0),
iced::Point::new(150.0, 300.0),
);
assert_eq!(
target,
Some(DropTarget::Join {
side: DockSide::Left,
group: 0,
index: 0
})
);
assert_eq!(
app.dock.left[0].panels,
vec![PanelId::BlockPalette, PanelId::Properties]
);
assert!(app.dock.right.is_empty());
// The bottom half of a pallet stacks below it.
let target = drag_panel(
&mut app,
id,
iced::Point::new(150.0, 100.0),
iced::Point::new(150.0, 800.0),
);
assert_eq!(
target,
Some(DropTarget::Join {
side: DockSide::Left,
group: 0,
index: 2
})
);
assert_eq!(
app.dock.left[0].panels,
vec![PanelId::Properties, PanelId::BlockPalette]
);
}
#[test]
fn dock_dragging_a_lone_pallet_over_its_own_group_is_a_no_op() {
use crate::app::config::DockSide;
use crate::ui::dock::{DropTarget, PanelId};
let mut app = dock_app();
let before = app.dock.clone();
// The right edge's only group holds just the block palette; drag it
// around over itself.
let target = drag_panel(
&mut app,
PanelId::BlockPalette,
iced::Point::new(1500.0, 10.0),
iced::Point::new(1450.0, 600.0),
);
assert_eq!(
target,
Some(DropTarget::Join {
side: DockSide::Right,
group: 0,
index: 1
})
);
assert_eq!(app.dock, before);
}
#[test]
fn dock_click_without_drag_changes_nothing() {
let mut app = dock_app();
let before = app.dock.clone();
let id = crate::ui::dock::PanelId::BlockPalette;
let p = iced::Point::new(1500.0, 10.0);
let target = drag_panel(&mut app, id, p, iced::Point::new(1502.0, 11.0));
assert_eq!(target, None, "movement under the threshold is a click");
assert_eq!(app.dock, before);
}
#[test]
fn dock_drop_below_the_strip_groups_starts_a_new_shown_group() {
use crate::app::config::DockSide;
use crate::ui::dock::{DropTarget, PanelId};
let mut app = dock_app();
let (_, plus_top) = app.dock_strip_layout(DockSide::Left);
let target = drag_panel(
&mut app,
PanelId::BlockPalette,
iced::Point::new(1500.0, 10.0),
iced::Point::new(18.0, plus_top + 10.0),
);
assert_eq!(
target,
Some(DropTarget::Edge {
side: DockSide::Left,
index: 1
})
);
assert_eq!(app.dock.left.len(), 2);
assert_eq!(app.dock.left[1].panels, vec![PanelId::BlockPalette]);
// The new group is the one shown: the column holds only the palette.
assert_eq!(app.dock_shown_group(DockSide::Left), Some(1));
assert_eq!(app.dock_slot_spans(DockSide::Left), vec![(0, 0.0, 900.0)]);
}
#[test]
fn dock_icon_press_switches_the_shown_group() {
use crate::app::config::DockSide;
use crate::ui::dock::{DockGroup, DockMsg, PanelId};
let mut app = dock_app();
app.show_browser = true;
app.dock.left = vec![
DockGroup::stack(vec![PanelId::Properties, PanelId::BlockPalette]),
DockGroup::single(PanelId::Browser),
];
app.dock.right.clear();
// Group 0 shows both stacked pallets.
assert_eq!(app.dock_slot_spans(DockSide::Left).len(), 2);
let _ = app.on_dock(DockMsg::IconPress(PanelId::Browser));
let _ = app.on_dock(DockMsg::DragRelease);
assert_eq!(app.dock_shown_group(DockSide::Left), Some(1));
assert_eq!(app.dock_slot_spans(DockSide::Left), vec![(0, 0.0, 900.0)]);
// Each group keeps its own width.
app.dock.set_group_width(DockSide::Left, 1, 400.0);
assert_eq!(app.dock_column_width(DockSide::Left), 400.0);
let _ = app.on_dock(DockMsg::IconPress(PanelId::BlockPalette));
let _ = app.on_dock(DockMsg::DragRelease);
assert_eq!(app.dock_column_width(DockSide::Left), 260.0);
}
#[test]
fn dock_drop_over_the_viewport_floats_the_panel() {
use crate::ui::dock::{DropTarget, PanelId};
let mut app = dock_app();
let target = drag_panel(
&mut app,
PanelId::BlockPalette,
iced::Point::new(1500.0, 10.0),
iced::Point::new(800.0, 400.0),
);
assert!(matches!(target, Some(DropTarget::Float { .. })));
assert!(app.dock.right.is_empty());
let f = app
.dock
.float_rect(PanelId::BlockPalette)
.expect("floating");
assert!(f.x > 600.0 && f.x < 800.0 && f.y > 350.0 && f.y < 400.0);
// Dragging a floating panel keeps the grab point under the pointer.
let target = drag_panel(
&mut app,
PanelId::BlockPalette,
iced::Point::new(f.x + 20.0, f.y + 10.0),
iced::Point::new(f.x + 120.0, f.y + 60.0),
);
assert_eq!(
target,
Some(DropTarget::Float {
x: f.x + 100.0,
y: f.y + 50.0
})
);
// Opening a floating panel must not re-dock it.
assert!(app.dock.is_placed(PanelId::BlockPalette));
}
#[test]
fn dock_dragging_an_icon_into_another_group_lands_between_its_icons() {
use crate::app::config::DockSide;
use crate::ui::dock::{DockGroup, DockMsg, DropTarget, PanelId, STRIP_CELL_H};
let mut app = dock_app();
app.show_browser = true;
app.dock.left = vec![
DockGroup::stack(vec![PanelId::Properties, PanelId::Browser]),
DockGroup::single(PanelId::BlockPalette),
];
app.dock.right.clear();
let (layout, _) = app.dock_strip_layout(DockSide::Left);
// Drag the Blocks icon (group 1) to between Properties and Browser.
let _ = app.on_dock(DockMsg::IconPress(PanelId::BlockPalette));
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(
20.0,
layout[1].icons_top + 10.0,
)));
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(
20.0,
layout[0].icons_top + STRIP_CELL_H + 2.0,
)));
assert_eq!(
drag_target(&app),
Some(DropTarget::Join {
side: DockSide::Left,
group: 0,
index: 1
})
);
let _ = app.on_dock(DockMsg::DragRelease);
assert_eq!(app.dock.left.len(), 1, "the emptied group goes away");
assert_eq!(
app.dock.left[0].panels,
vec![PanelId::Properties, PanelId::BlockPalette, PanelId::Browser]
);
}
#[test]
fn dock_grip_drag_moves_a_whole_group() {
use crate::app::config::DockSide;
use crate::ui::dock::{DockGroup, DockMsg, PanelId};
let mut app = dock_app();
app.show_browser = true;
app.dock.left = vec![
DockGroup::stack(vec![PanelId::Properties, PanelId::Browser]),
DockGroup::single(PanelId::BlockPalette),
];
app.dock.right.clear();
let (layout, plus_top) = app.dock_strip_layout(DockSide::Left);
let _ = app.on_dock(DockMsg::GroupGrab(DockSide::Left, 0));
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(
18.0,
layout[0].top + 2.0,
)));
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(18.0, plus_top + 10.0)));
let _ = app.on_dock(DockMsg::DragRelease);
assert_eq!(app.dock.left[0].panels, vec![PanelId::BlockPalette]);
assert_eq!(
app.dock.left[1].panels,
vec![PanelId::Properties, PanelId::Browser]
);
// A group dropped over the viewport does not float or split up.
let before = app.dock.clone();
let _ = app.on_dock(DockMsg::GroupGrab(DockSide::Left, 1));
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(18.0, 300.0)));
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(800.0, 400.0)));
assert_eq!(drag_target(&app), None);
let _ = app.on_dock(DockMsg::DragRelease);
assert_eq!(app.dock, before);
}
#[test]
fn dock_closed_pallets_take_no_room_and_empty_groups_hide() {
use crate::app::config::DockSide;
use crate::ui::dock::{DockGroup, PanelId};
let mut app = dock_app();
app.dock.left = vec![
DockGroup::stack(vec![PanelId::BlockPalette, PanelId::Properties]),
DockGroup::single(PanelId::Browser),
];
app.show_block_palette = false;
app.show_browser = false;
// Only Properties is open: one listed group, full height.
assert_eq!(app.dock_visible_groups(DockSide::Left), vec![0]);
assert_eq!(app.dock_slot_spans(DockSide::Left), vec![(1, 0.0, 900.0)]);
// A shown group with nothing open falls back to one that has.
app.dock.show_group(DockSide::Left, 1);
assert_eq!(app.dock_shown_group(DockSide::Left), Some(0));
}
#[test]
fn dock_splitter_drag_changes_pallet_heights() {
use crate::app::config::DockSide;
use crate::ui::dock::{DockGroup, DockMsg, PanelId};
let mut app = dock_app();
app.dock.left = vec![DockGroup::stack(vec![
PanelId::BlockPalette,
PanelId::Properties,
])];
app.dock.right.clear();
let _ = app.on_dock(DockMsg::SplitGrab(DockSide::Left, 0, 0, 1));
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(100.0, 450.0)));
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(100.0, 675.0)));
let _ = app.on_dock(DockMsg::DragRelease);
// 225 px of a 900 px edge holding 2 weight units = 0.5 units.
let w = &app.dock.left[0].weights;
assert!((w[0] - 1.5).abs() < 1e-4 && (w[1] - 0.5).abs() < 1e-4);
let _ = app.on_dock(DockMsg::SplitReset(DockSide::Left, 0));
assert_eq!(app.dock.left[0].weights, vec![1.0, 1.0]);
}
#[test]
fn dock_hover_reveals_groups_only_on_an_auto_hiding_edge() {
use crate::app::config::DockSide;
use crate::ui::dock::{DockGroup, DockMsg, PanelId};
let mut app = dock_app();
app.show_browser = true;
app.dock.left.push(DockGroup::single(PanelId::Browser));
// The pin on any docked pallet turns auto-hide on for its edge.
let _ = app.on_dock(DockMsg::AutoCollapseToggle(PanelId::Properties));
assert!(app.dock.edge_auto_hide(DockSide::Left));
assert!(!app.dock.edge_auto_hide(DockSide::Right));
assert!(!app.dock_edge_expanded(DockSide::Left));
// Hovering an icon flies its group out.
let _ = app.on_dock(DockMsg::Hover(PanelId::Browser));
// ...but only once the pointer rests: the reveal waits for the delay.
assert!(!app.dock_edge_expanded(DockSide::Left));
let _ = app.on_dock(DockMsg::HoverSettled(app.dock_hover_gen));
assert!(app.dock_edge_expanded(DockSide::Left));
assert_eq!(app.dock_shown_group(DockSide::Left), Some(1));
// Leaving and coming back within the delay keeps it open.
let _ = app.on_dock(DockMsg::HoverExit);
let stale = app.dock_hover_gen;
let _ = app.on_dock(DockMsg::HoverStay);
let _ = app.on_dock(DockMsg::HoverSettled(stale));
assert!(app.dock_edge_expanded(DockSide::Left));
let _ = app.on_dock(DockMsg::HoverExit);
let _ = app.on_dock(DockMsg::HoverSettled(app.dock_hover_gen));
assert!(!app.dock_edge_expanded(DockSide::Left));
// Without auto-hide, hovering does not switch groups (clicks do).
let _ = app.on_dock(DockMsg::AutoCollapseToggle(PanelId::Properties));
let _ = app.on_dock(DockMsg::Hover(PanelId::Properties));
assert_eq!(app.dock_shown_group(DockSide::Left), Some(1));
assert!(app.dock_edge_expanded(DockSide::Left));
}
#[test]
fn dock_auto_hide_waits_for_an_open_layer_color_dropdown() {
use crate::ui::dock::{DockGroup, DockMsg, PanelId};
let mut app = dock_app();
app.dock.left.push(DockGroup::single(PanelId::Layers));
let _ = app.on_dock(DockMsg::AutoCollapseToggle(PanelId::Layers));
let _ = app.on_dock(DockMsg::Hover(PanelId::Layers));
let _ = app.on_dock(DockMsg::HoverSettled(app.dock_hover_gen));
assert_eq!(app.dock_peek, Some(PanelId::Layers));
// Opening a row's colour swatch, then moving onto its dropdown, reads
// as the pointer leaving the edge: the panel must stay out.
let _ = app.update(Message::LayerColorPickerToggle("0".to_string()));
assert!(app.tabs[app.active_tab].layers.color_picker_row.is_some());
let _ = app.on_dock(DockMsg::HoverExit);
let _ = app.on_dock(DockMsg::HoverSettled(app.dock_hover_gen));
assert_eq!(app.dock_peek, Some(PanelId::Layers));
let _ = app.on_dock(DockMsg::HoverSettled(app.dock_hover_gen));
assert_eq!(app.dock_peek, Some(PanelId::Layers));
// Once the dropdown closes with the pointer still away, it hides.
let _ = app.update(Message::LayerColorPickerToggle("0".to_string()));
let _ = app.on_dock(DockMsg::HoverSettled(app.dock_hover_gen));
assert_eq!(app.dock_peek, None);
}
#[test]
fn dock_float_resize_grows_the_panel() {
use crate::ui::dock::{DockMsg, FloatPanel, PanelId};
let mut app = dock_app();
app.dock.float(FloatPanel::new(
PanelId::BlockPalette,
100.0,
100.0,
260.0,
300.0,
));
let _ = app.on_dock(DockMsg::FloatResizeGrab(PanelId::BlockPalette, false));
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(360.0, 400.0)));
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(400.0, 450.0)));
let _ = app.on_dock(DockMsg::DragRelease);
let f = app.dock.float_rect(PanelId::BlockPalette).unwrap();
assert_eq!((f.w, f.h), (300.0, 350.0));
// The bottom-left grip grows the panel leftward, keeping its right edge.
let _ = app.on_dock(DockMsg::FloatResizeGrab(PanelId::BlockPalette, true));
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(100.0, 450.0)));
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(60.0, 450.0)));
let _ = app.on_dock(DockMsg::DragRelease);
let g = app.dock.float_rect(PanelId::BlockPalette).unwrap();
assert_eq!((g.x, g.w, g.h), (f.x - 40.0, 340.0, 350.0));
}
#[test]
fn dock_edge_menu_adds_and_hides_pallets() {
use crate::app::config::DockSide;
use crate::ui::dock::{DockMsg, PanelId};
let mut app = dock_app();
app.show_browser = false;
let _ = app.on_dock(DockMsg::EdgeMenu(Some(DockSide::Left)));
// Checking a closed pallet opens it as a new, shown group here.
let _ = app.on_dock(DockMsg::EdgeMenuToggle(DockSide::Left, PanelId::Browser));
assert!(app.show_browser);
assert_eq!(app.dock.left.len(), 2);
assert_eq!(app.dock.left[1].panels, vec![PanelId::Browser]);
assert_eq!(app.dock_shown_group(DockSide::Left), Some(1));
assert_eq!(app.dock_edge_menu, None, "the menu closes after adding");
let _ = app.on_dock(DockMsg::EdgeMenu(Some(DockSide::Left)));
// Unchecking hides it but keeps its place.
let _ = app.on_dock(DockMsg::EdgeMenuToggle(DockSide::Left, PanelId::Browser));
assert!(!app.show_browser);
assert_eq!(
app.dock.location(PanelId::Browser),
Some((DockSide::Left, 1))
);
// Opening it again from the same edge reuses that place.
let _ = app.on_dock(DockMsg::EdgeMenuToggle(DockSide::Left, PanelId::Browser));
assert_eq!(app.dock.left.len(), 2);
// A pallet open on the other edge is not checked here: picking it
// moves it over instead of hiding it.
let _ = app.on_dock(DockMsg::EdgeMenuToggle(
DockSide::Left,
PanelId::BlockPalette,
));
assert!(app.show_block_palette);
assert!(app.dock.right.is_empty());
assert_eq!(
app.dock.location(PanelId::BlockPalette),
Some((DockSide::Left, 2))
);
}
#[test]
fn dock_divider_resizes_only_its_group() {
use crate::app::config::DockSide;
use crate::ui::dock::{DockGroup, DockMsg, PanelId};
let mut app = dock_app();
app.dock.left = vec![
DockGroup::stack(vec![PanelId::Properties, PanelId::BlockPalette]),
DockGroup::single(PanelId::Count),
];
app.dock.right.clear();
let _ = app.on_dock(DockMsg::ResizeGrab(PanelId::Properties));
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(300.0, 100.0)));
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(330.0, 100.0)));
let _ = app.on_dock(DockMsg::DragRelease);
// The group was 260 wide (its widest pallet); it is now 290, while
// the other group and the pallets' own widths are untouched.
assert_eq!(app.dock_group_width(DockSide::Left, 0), 290.0);
assert_eq!(app.dock_group_width(DockSide::Left, 1), 280.0);
assert_eq!(app.dock.settings(PanelId::Properties).width, 250.0);
}
#[test]
fn layers_command_opens_a_floating_pallet_and_toggles_it_closed() {
use crate::ui::dock::PanelId;
let mut app = dock_app();
let _ = app.update(Message::ToggleLayers);
assert!(app.show_layers);
assert!(app.active_modal.is_none(), "no modal backdrop");
let f = app
.dock
.float_rect(PanelId::Layers)
.expect("floats on first use");
assert!(f.x > 0.0 && f.y >= 0.0);
let _ = app.update(Message::ToggleLayers);
assert!(!app.show_layers);
// Its place is kept for next time.
assert!(app.dock.float_rect(PanelId::Layers).is_some());
}
/// A headless renderer for driving the real view.
fn headless_renderer() -> iced::Renderer {
iced_test::futures::futures::executor::block_on(
<iced::Renderer as iced_test::core::renderer::Headless>::new(
iced_test::core::renderer::Settings::default(),
None,
),
)
.expect("headless renderer")
}
/// Size the real view is laid out at.
const VIEW_SIZE: iced::Size = iced::Size::new(1600.0, 900.0);
/// Centre of the first text `label` in the real view.
fn find_in_view(app: &OpenCADStudio, label: &str) -> iced::Point {
use iced_test::core::widget;
use iced_test::runtime::user_interface::{Cache, UserInterface};
use iced_test::Selector;
let mut renderer = headless_renderer();
let mut ui = UserInterface::build(app.view_main(), VIEW_SIZE, Cache::default(), &mut renderer);
let mut find = Selector::find(label);
ui.operate(&renderer, &mut widget::operation::black_box(&mut find));
match widget::Operation::finish(&find) {
widget::operation::Outcome::Some(Some(target)) => {
target.visible_bounds().expect("visible").center()
}
_ => panic!("{label} not found"),
}
}
/// Drive the real view through iced's `UserInterface` with the pointer at
/// `at`: feed `events` one at a time, and between events hand the
/// published messages to the app and rebuild the view while keeping the
/// widget-tree cache, as the runtime does.
fn drive_view(app: &mut OpenCADStudio, at: iced::Point, events: &[iced::Event]) {
use iced_test::core::{mouse, shell, window};
use iced_test::runtime::user_interface::{Cache, UserInterface};
let mut renderer = headless_renderer();
let mut cache = Cache::default();
let cursor = mouse::Cursor::Available(at);
for event in events {
let mut messages = Vec::new();
{
let mut ui = UserInterface::build(app.view_main(), VIEW_SIZE, cache, &mut renderer);
let _ = ui.update(
&window::Headless,
&shell::Waker::noop(),
std::slice::from_ref(event),
cursor,
&mut renderer,
&mut messages,
);
cache = ui.into_cache();
}
for message in messages {
let _ = app.update(message);
}
}
}
/// The hover messages an open dropdown inside a hover zone (`zone`) or a
/// `mouse_area` publishes as the pointer goes from the field onto the
/// dropdown's list, then out of the area, each tagged with where the pointer
/// was.
fn hover_messages_over_an_open_dropdown(zone: bool) -> Vec<(&'static str, &'static str)> {
use crate::ui::dock::DockMsg;
use iced::widget::{button, column, container, mouse_area, text};
use iced_test::core::{mouse, shell, window};
use iced_test::runtime::user_interface::{Cache, UserInterface};
let view = || -> iced::Element<'static, Message> {
let field = button(text("ByLayer")).width(120).height(20);
// A pointer-wanting list, like a linetype combo's, under the field.
let list = button(column![text("Continuous"), text("Dashed")])
.on_press(Message::Dock(DockMsg::HoverEnd(
crate::ui::dock::PanelId::Layers,
)))
.width(120)
.height(100);
let dd = iced_aw::DropDown::new(field, list, true)
.alignment(iced_aw::drop_down::Alignment::Bottom);
let area = container(dd).width(300).height(400);
let (enter, exit) = (
Message::Dock(DockMsg::HoverStay),
Message::Dock(DockMsg::HoverExit),
);
if zone {
crate::ui::hover_zone::hover_zone(area, enter, exit)
} else {
mouse_area(area).on_enter(enter).on_exit(exit).into()
}
};
let mut renderer = headless_renderer();
let mut cache = Cache::default();
let mut seen = Vec::new();
// On the field, on the open list (both inside the area), then outside.
for (place, x, y) in [
("field", 60.0, 10.0),
("list", 60.0, 70.0),
("list", 60.0, 90.0),
("outside", 500.0, 70.0),
] {
let at = iced::Point::new(x, y);
let mut messages = Vec::new();
let mut ui = UserInterface::build(view(), VIEW_SIZE, cache, &mut renderer);
let _ = ui.update(
&window::Headless,
&shell::Waker::noop(),
&[iced::Event::Mouse(mouse::Event::CursorMoved {
position: at,
})],
mouse::Cursor::Available(at),
&mut renderer,
&mut messages,
);
cache = ui.into_cache();
seen.extend(messages.into_iter().filter_map(|m| match m {
Message::Dock(DockMsg::HoverStay) => Some(("enter", place)),
Message::Dock(DockMsg::HoverExit) => Some(("exit", place)),
_ => None,
}));
}
seen
}
#[test]
fn an_open_dropdown_in_a_hover_zone_does_not_read_as_leaving() {
with_stack(|| {
// The bug: a mouse area reads the pointer on the list as gone.
assert_eq!(
hover_messages_over_an_open_dropdown(false),
[("enter", "field"), ("exit", "list")]
);
assert_eq!(
hover_messages_over_an_open_dropdown(true),
[("enter", "field"), ("exit", "outside")]
);
});
}
/// Bounds of the field (text `ByLayer`) and of its open list (text `Opaque`)
/// for a `drop_down_below` in a scrollable that, like a pallet, sits away
/// from the window's top-left corner.
fn drop_down_below_in_an_offset_scrollable() -> (iced::Rectangle, iced::Rectangle) {
use iced::widget::{button, column, container, row, scrollable, text, Space};
use iced_test::core::widget;
use iced_test::runtime::user_interface::{Cache, UserInterface};
use iced_test::Selector;
let view = || -> iced::Element<'static, Message> {
let field = container(text("ByLayer")).width(200).height(20);
let list = column![
button(text("Opaque")).width(iced::Fill),
button(text("50")).width(iced::Fill),
];
let dd = crate::ui::color_select::drop_down_below(
field.into(),
list.into(),
None,
iced::Length::Shrink,
Message::PropEditChoiceToggle,
);
let pallet = scrollable(column![Space::new().height(300), dd]).height(500);
row![
Space::new().width(1000),
container(pallet).width(300).height(iced::Fill)
]
.into()
};
let mut renderer = headless_renderer();
let mut ui = UserInterface::build(view(), VIEW_SIZE, Cache::default(), &mut renderer);
let mut find = |label: &str| {
let mut find = Selector::find(label);
ui.operate(&renderer, &mut widget::operation::black_box(&mut find));
match widget::Operation::finish(&find) {
widget::operation::Outcome::Some(Some(target)) => target.bounds(),
_ => panic!("{label} not found"),
}
};
(find("ByLayer"), find("Opaque"))
}
#[test]
fn a_drop_down_in_a_pallet_opens_right_below_its_field() {
with_stack(|| {
let (field, item) = drop_down_below_in_an_offset_scrollable();
assert!(
item.y >= field.y + field.height && item.y < field.y + field.height + 20.0,
"list starts below the field: field {field:?}, first item {item:?}"
);
assert!(
item.x >= field.x - 1.0 && item.x < field.x + field.width,
"list lines up with the field: field {field:?}, first item {item:?}"
);
});
}
#[test]
fn docked_title_bar_double_click_floats_the_pallet_in_the_real_view() {
// Building the real view is deeper than a default test thread's stack.
with_stack(|| {
use crate::ui::dock::PanelId;
let mut app = dock_app();
let press =
iced::Event::Mouse(iced::mouse::Event::ButtonPressed(iced::mouse::Button::Left));
let release = iced::Event::Mouse(iced::mouse::Event::ButtonReleased(
iced::mouse::Button::Left,
));
// The block palette's title text sits in its docked title bar.
let title = find_in_view(&app, "Blocks");
drive_view(
&mut app,
title,
&[press.clone(), release.clone(), press, release],
);
assert!(
app.dock.float_rect(PanelId::BlockPalette).is_some(),
"a double-click on a docked title bar floats the pallet"
);
});
}
#[test]
fn hovering_a_strip_icon_reveals_an_auto_hiding_edge_in_the_real_view() {
// Building the real view is deeper than a default test thread's stack.
with_stack(|| {
use crate::app::config::DockSide;
use crate::ui::dock::{DockMsg, PanelId, STRIP_CELL_H, STRIP_PAD};
let mut app = dock_app();
// Mirror the layout onto the left so the block palette's title marks
// where the workspace starts, then auto-hide the right edge.
let title = find_in_view(&app, "Blocks");
let _ = app.on_dock(DockMsg::AutoCollapseToggle(PanelId::BlockPalette));
assert!(!app.dock_edge_expanded(DockSide::Right));
// The title bar text sits ~13 px below the workspace top; the strip's
// first icon is centred STRIP_PAD + half a cell below it.
let workspace_top = title.y - 13.0;
let icon = iced::Point::new(
VIEW_SIZE.width - crate::ui::dock::DOCK_STRIP_W / 2.0,
workspace_top + STRIP_PAD + STRIP_CELL_H / 2.0,
);
drive_view(
&mut app,
icon,
&[iced::Event::Mouse(iced::mouse::Event::CursorMoved {
position: icon,
})],
);
let _ = app.on_dock(DockMsg::HoverSettled(app.dock_hover_gen));
assert!(
app.dock_edge_expanded(DockSide::Right),
"hovering the icon reveals the auto-hiding edge"
);
});
}
#[test]
fn layers_ribbon_button_opens_the_pallet_without_staying_highlighted() {
let mut app = dock_app();
// The ribbon's LAYERS button runs the LAYERS command.
let click = || Message::RibbonToolClick {
tool_id: "LAYERS".to_string(),
event: crate::modules::ModuleEvent::Command("LAYERS".to_string()),
};
let _ = app.update(click());
assert!(app.show_layers);
assert_eq!(app.ribbon.active_tool(), None);
// Pressing it again keeps it open, like the Blocks palette's button.
let _ = app.update(click());
assert!(app.show_layers);
}
#[test]
fn dock_double_clicks_float_and_dock_panels() {
use crate::app::config::DockSide;
use crate::ui::dock::{DockMsg, PanelId};
let mut app = dock_app();
let _ = app.on_dock(DockMsg::FloatOut(PanelId::Properties));
assert!(app.dock.left.is_empty());
let f = app.dock.float_rect(PanelId::Properties).expect("floating");
assert!(f.x > 0.0, "floats clear of the left edge");
let _ = app.on_dock(DockMsg::DockTo(PanelId::Properties, DockSide::Right));
assert!(app.dock.floating.is_empty());
assert_eq!(
app.dock.location(PanelId::Properties),
Some((DockSide::Right, 1))
);
}
#[test]
fn blockpalette_width_reset() {
use crate::app::config::DockSide;
let mut app = dock_app();
let id = crate::ui::dock::PanelId::BlockPalette;
app.dock.set_group_width(DockSide::Right, 0, 500.0);
let _ = app.on_dock(crate::ui::dock::DockMsg::WidthReset(id));
assert_eq!(app.dock.group_width(DockSide::Right, 0), 260.0);
}
#[test]
fn floating_menu_docks_and_allow_docking_keeps_a_drag_floating() {
use crate::app::config::DockSide;
use crate::ui::dock::{DockMsg, DropTarget, PanelId};
let mut app = dock_app();
let _ = app.on_dock(DockMsg::FloatOut(PanelId::BlockPalette));
// With Allow docking off, a drag onto the left edge's strip stays
// floating.
let _ = app.on_dock(DockMsg::FloatMenu(Some(PanelId::BlockPalette)));
let _ = app.on_dock(DockMsg::FloatDockingToggle(PanelId::BlockPalette));
assert_eq!(app.dock_float_menu, None);
assert!(!app.dock.float_rect(PanelId::BlockPalette).unwrap().docking);
let _ = app.on_dock(DockMsg::DockGrab(PanelId::BlockPalette));
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(500.0, 300.0)));
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(10.0, 60.0)));
assert!(matches!(drag_target(&app), Some(DropTarget::Float { .. })));
let _ = app.on_dock(DockMsg::DragRelease);
let f = app
.dock
.float_rect(PanelId::BlockPalette)
.expect("still floating");
assert!(!f.docking, "moving it keeps the choice");
// The menu's Dock left still docks it.
let _ = app.on_dock(DockMsg::FloatMenu(Some(PanelId::BlockPalette)));
let _ = app.on_dock(DockMsg::DockTo(PanelId::BlockPalette, DockSide::Left));
assert_eq!(app.dock_float_menu, None);
assert_eq!(
app.dock.location(PanelId::BlockPalette).map(|(s, _)| s),
Some(DockSide::Left)
);
}
#[test]
fn visibility_ribbon_real_controls_disable_enable_and_open_manager() {
with_stack(|| {
use crate::ui::window::block_visibility::Action as A;
let mut app = authoring_app();
app.ribbon.set_block_editing(true);
app.ribbon
.set_collapse_mode(crate::ui::ribbon::CollapseMode::Compact);
let i = app.active_tab;
let before = app.tabs[i].scene.document.clone();
let point = authoring_control(&app, "BVSTATE_MANAGER");
authoring_click(&mut app, point);
assert!(app.block_visibility.is_none());
assert_eq!(app.tabs[i].scene.document, before);
authoring_capture(&app, "visibility-ribbon-disabled");
let _ = app.run_command_line("BPARAMETER VISIBILITY 0.5,0.5");
let target = app.block_visibility_context().unwrap().target;
let point = authoring_control(&app, "BVSTATE_MANAGER");
authoring_click(&mut app, point);
assert_eq!(
app.active_modal,
Some(crate::app::ModalKind::BlockVisibility)
);
let _ = app.update(Message::BlockVisibility(A::Name("Alternate".into())));
let _ = app.update(Message::BlockVisibility(A::New));
authoring_capture(&app, "visibility-states-dialog");
let point = find_in_view(&app, "OK");
authoring_click(&mut app, point);
assert!(app.block_visibility.is_none());
assert_eq!(app.block_visibility_context().unwrap().names.len(), 2);
let messages = {
let mut simulator = iced_test::Simulator::with_size(
iced::Settings::default(),
VIEW_SIZE,
app.view_main(),
);
let target = simulator
.click(iced::widget::Id::new("BVSTATE_CURRENT"))
.expect("current-state selector");
let bounds = target.visible_bounds().unwrap();
authoring_snapshot(
&mut simulator,
&app.active_theme,
"visibility-state-dropdown",
);
// Native pick-list options are painted text, not Text widgets.
// Pick the second row using the measured selector/row height.
let position = iced::Point::new(bounds.center_x(), bounds.y + bounds.height * 2.5);
simulator.point_at(position);
simulator.simulate([
iced::Event::Mouse(iced::mouse::Event::CursorMoved { position }),
iced::Event::Mouse(iced::mouse::Event::ButtonPressed(iced::mouse::Button::Left)),
iced::Event::Mouse(iced::mouse::Event::ButtonReleased(
iced::mouse::Button::Left,
)),
]);
simulator.into_messages().collect::<Vec<_>>()
};
for message in messages {
let _ = app.update(message);
}
assert_eq!(
app.block_visibility_context().unwrap().current.as_deref(),
Some("Alternate")
);
let point = authoring_control(&app, "BVMODE_TOGGLE");
authoring_click(&mut app, point);
assert!(app.block_visibility_mode);
authoring_capture(&app, "visibility-ribbon-enabled");
app.ribbon
.set_collapse_mode(crate::ui::ribbon::CollapseMode::Collapsed);
let point = find_in_view(&app, "Visibility ▾");
authoring_click(&mut app, point);
let messages = {
let mut simulator = iced_test::Simulator::with_size(
iced::Settings::default(),
VIEW_SIZE,
app.view_main(),
);
simulator
.click("Visibility States")
.expect("manager in flyout");
simulator.into_messages().collect::<Vec<_>>()
};
for message in messages {
let _ = app.update(message);
}
assert_eq!(app.block_visibility.as_ref().unwrap().target, target);
});
}
#[test]
fn point_parameter_placement_starts_from_the_actual_palette_row() {
with_stack(|| {
let mut app = authoring_app();
let _ = app.dispatch_command("BAUTHORPALETTE");
let i = app.active_tab;
let before = app.tabs[i].scene.document.clone();
let point = authoring_control(&app, "authoring-tool-point");
authoring_click(&mut app, point);
assert_eq!(
app.tabs[i].active_cmd.as_ref().unwrap().name(),
"BPARAMETER"
);
let _ = app.update(Message::CommandInput("0.25,0.25".into()));
let _ = app.update(Message::CommandSubmit);
assert_eq!(app.tabs[i].scene.document, before);
let _ = app.update(Message::CommandInput("0.65,0.15".into()));
let _ = app.update(Message::CommandSubmit);
assert!(
app.tabs[i].scene.preview_wires.is_empty(),
"placement clears its rubber-band preview"
);
let definition = app.tabs[i].active_block_edit_session().unwrap().br_handle;
assert_eq!(
crate::scene::block_point_parameter::parameters(
&app.tabs[i].scene.document,
definition
)
.len(),
1
);
assert!(
app.visibility_placement_error().is_none(),
"Visibility can join the Point graph"
);
assert_eq!(app.tabs[i].history.undo_stack.len(), 1);
authoring_capture(&app, "point-parameter-placed");
let point = authoring_control(&app, "authoring-tool-point");
authoring_click(&mut app, point);
let _ = app.update(Message::CommandEscape);
assert_eq!(app.tabs[i].history.undo_stack.len(), 1);
});
}
#[test]
fn distance_parameter_batch_starts_from_actual_palette_rows() {
with_stack(|| {
for (id, name) in [
("authoring-tool-linear", "linear"),
("authoring-tool-polar", "polar"),
("authoring-tool-xy", "xy"),
] {
let mut app = authoring_app();
let _ = app.dispatch_command("BAUTHORPALETTE");
let point = authoring_control(&app, id);
authoring_click(&mut app, point);
let i = app.active_tab;
let before = app.tabs[i].scene.document.clone();
for text in ["0.25,-0.15", "0.65,0.15"] {
let _ = app.update(Message::CommandInput(text.into()));
let _ = app.update(Message::CommandSubmit);
assert_eq!(app.tabs[i].scene.document, before);
}
let _ = app.update(Message::CommandInput("0.3,0.2".into()));
let _ = app.update(Message::CommandSubmit);
let definition = app.tabs[i].active_block_edit_session().unwrap().br_handle;
let found = crate::scene::block_distance_parameter::parameters(
&app.tabs[i].scene.document,
definition,
);
assert_eq!(found.len(), 1);
assert!(found[0].kind.name().eq_ignore_ascii_case(name));
assert_eq!(app.tabs[i].history.undo_stack.len(), 1);
assert!(app.tabs[i].scene.preview_wires.is_empty());
let _ = app.run_command_line("ZOOM E");
authoring_capture(&app, &format!("{name}-parameter-placed"));
}
});
}
#[test]
fn orientation_parameter_batch_starts_from_actual_palette_rows() {
with_stack(|| {
for (id, name, inputs) in [
(
"authoring-tool-rotation",
"rotation",
vec!["0.35,-0.12", "0.65,-0.12", "0.35,0.2"],
),
(
"authoring-tool-alignment",
"alignment",
vec!["0.25,-0.1", "0.6,0.15"],
),
(
"authoring-tool-flip",
"flip",
vec!["0.35,-0.15", "0.35,0.15", "0.6,0.15"],
),
] {
let mut app = authoring_app();
let _ = app.dispatch_command("BAUTHORPALETTE");
let point = authoring_control(&app, id);
authoring_click(&mut app, point);
let i = app.active_tab;
let before = app.tabs[i].scene.document.clone();
for text in &inputs[..inputs.len() - 1] {
let _ = app.update(Message::CommandInput((*text).into()));
let _ = app.update(Message::CommandSubmit);
assert_eq!(app.tabs[i].scene.document, before);
}
let _ = app.update(Message::CommandInput(inputs.last().unwrap().to_string()));
let _ = app.update(Message::CommandSubmit);
let definition = app.tabs[i].active_block_edit_session().unwrap().br_handle;
let found = crate::scene::block_orientation_parameter::parameters(
&app.tabs[i].scene.document,
definition,
);
assert_eq!(found.len(), 1);
assert!(found[0].kind.name().eq_ignore_ascii_case(name));
assert!(app.tabs[i].scene.preview_wires.is_empty());
assert_eq!(app.tabs[i].history.undo_stack.len(), 1);
let _ = app.run_command_line("ZOOM E");
authoring_capture(&app, &format!("{name}-parameter-placed"));
}
});
}
fn authoring_app() -> OpenCADStudio {
use codec::{
entities::{Insert, Line},
types::Vector3,
EntityType,
};
let mut app = dock_app();
let i = app.active_tab;
app.tabs[i].scene.define_block_raw(
"AUTHOR",
Vector3::ZERO,
vec![EntityType::Line(Line::from_points(
Vector3::ZERO,
Vector3::UNIT_X,
))],
);
let reference = app.tabs[i]
.scene
.document
.add_entity(EntityType::Insert(Insert::new("AUTHOR", Vector3::ZERO)))
.unwrap();
let _ = app.dispatch_command(&format!("BEDIT_BEGIN:{}", reference.value()));
app
}
#[test]
fn authoring_palettes_ribbon_toggle_and_dock_close_leave_document_and_history_unchanged() {
use crate::ui::dock::{DockMsg, PanelId};
let mut app = authoring_app();
let i = app.active_tab;
let before = app.tabs[i].scene.document.clone();
let history = app.tabs[i].history.undo_stack.len();
let click = || Message::RibbonToolClick {
tool_id: "BAUTHORPALETTE".into(),
event: crate::modules::ModuleEvent::Command("BAUTHORPALETTE".into()),
};
let _ = app.update(click());
assert!(app.dock_panel_visible(PanelId::AuthoringPalettes));
assert!(app.ribbon.show_authoring_palettes);
assert!(app.dock.is_placed(PanelId::AuthoringPalettes));
let _ = app.update(click());
assert!(!app.dock_panel_visible(PanelId::AuthoringPalettes));
assert!(!app.ribbon.show_authoring_palettes);
let _ = app.dispatch_command("BAUTHORPALETTE");
let _ = app.on_dock(DockMsg::Close(PanelId::AuthoringPalettes));
assert!(!app.show_authoring_palettes);
assert!(!app.ribbon.show_authoring_palettes);
assert_eq!(app.tabs[i].scene.document, before);
assert_eq!(app.tabs[i].history.undo_stack.len(), history);
assert!(!app.block_edit_dirty(i));
}
#[test]
fn authoring_palettes_follow_editor_context_and_keep_the_docked_placement() {
use crate::app::config::DockSide;
use crate::ui::dock::{DockMsg, PanelId};
let mut app = authoring_app();
let _ = app.dispatch_command("BAUTHORPALETTE");
let _ = app.on_dock(DockMsg::DockTo(PanelId::AuthoringPalettes, DockSide::Left));
let placement = app.dock.location(PanelId::AuthoringPalettes);
let _ = app.dispatch_command("BTESTBLOCK");
assert!(!app.dock_panel_visible(PanelId::AuthoringPalettes));
let _ = app.dispatch_command("BCLOSETESTBLOCK");
assert!(app.dock_panel_visible(PanelId::AuthoringPalettes));
let _ = app.dispatch_command("BTESTBLOCK");
assert!(!app.dock_panel_visible(PanelId::AuthoringPalettes));
let _ = app.dispatch_command("BAUTHORPALETTECLOSE");
let _ = app.dispatch_command("BAUTHORPALETTE");
assert!(!app.show_authoring_palettes);
assert!(!app.dock_panel_visible(PanelId::AuthoringPalettes));
let _ = app.dispatch_command("BCLOSETESTBLOCK");
let _ = app.dispatch_command("BAUTHORPALETTE");
assert!(app.dock_panel_visible(PanelId::AuthoringPalettes));
assert_eq!(app.dock.location(PanelId::AuthoringPalettes), placement);
let _ = app.dispatch_command("BEDIT_DISCARD");
assert!(!app.dock_panel_visible(PanelId::AuthoringPalettes));
let _ = app.dispatch_command("BAUTHORPALETTECLOSE");
let _ = app.dispatch_command("BAUTHORPALETTE");
assert!(!app.show_authoring_palettes);
}
#[test]
fn authoring_palettes_toggle_keeps_the_running_drawing_command() {
let mut app = authoring_app();
let _ = app.dispatch_command("LINE");
assert!(app.tabs[app.active_tab].active_cmd.is_some());
let _ = app.update(Message::RibbonToolClick {
tool_id: "BAUTHORPALETTE".into(),
event: crate::modules::ModuleEvent::Command("BAUTHORPALETTE".into()),
});
assert!(app.tabs[app.active_tab].active_cmd.is_some());
let _ = app.dispatch_command("BAUTHORPALETTECLOSE");
assert!(app.tabs[app.active_tab].active_cmd.is_some());
}
#[test]
fn authoring_palettes_catalog_in_real_view_has_no_parameter_placement_action() {
with_stack(|| {
let mut app = authoring_app();
let _ = app.dispatch_command("BAUTHORPALETTE");
let point = find_in_view(&app, "Basepoint");
let before = app.tabs[app.active_tab].scene.document.clone();
drive_view(
&mut app,
point,
&[
iced::Event::Mouse(iced::mouse::Event::ButtonPressed(iced::mouse::Button::Left)),
iced::Event::Mouse(iced::mouse::Event::ButtonReleased(
iced::mouse::Button::Left,
)),
],
);
assert_eq!(app.tabs[app.active_tab].scene.document, before);
assert!(app.tabs[app.active_tab].active_cmd.is_none());
});
}
/// Locate rotated category controls by their stable widget IDs.
fn authoring_control(app: &OpenCADStudio, id: &'static str) -> iced::Point {
use iced_test::core::widget;
use iced_test::runtime::user_interface::{Cache, UserInterface};
use iced_test::Selector;
let mut renderer = headless_renderer();
let mut ui = UserInterface::build(app.view_main(), VIEW_SIZE, Cache::default(), &mut renderer);
let mut find = widget::Id::new(id).find();
ui.operate(&renderer, &mut widget::operation::black_box(&mut find));
match widget::Operation::finish(&find) {
widget::operation::Outcome::Some(Some(target)) => {
target.visible_bounds().expect("visible").center()
}
_ => panic!("{id} not found"),
}
}
fn authoring_click(app: &mut OpenCADStudio, point: iced::Point) {
drive_view(
app,
point,
&[
iced::Event::Mouse(iced::mouse::Event::ButtonPressed(iced::mouse::Button::Left)),
iced::Event::Mouse(iced::mouse::Event::ButtonReleased(
iced::mouse::Button::Left,
)),
],
);
}
#[test]
fn authoring_palettes_real_tabs_switch_catalogs_and_unavailable_rows_stay_inert() {
with_stack(|| {
use crate::ui::window::authoring_palettes::Tab;
let mut app = authoring_app();
let _ = app.dispatch_command("BAUTHORPALETTE");
let i = app.active_tab;
let before = app.tabs[i].scene.document.clone();
let history = app.tabs[i].history.undo_stack.len();
let _ = app.dispatch_command("LINE");
for (tab, row) in [
(Tab::Actions, "Block Properties Table"),
(Tab::ParameterSets, "Point Move"),
(Tab::Parameters, "Basepoint"),
] {
let point = authoring_control(&app, tab.widget_id());
authoring_click(&mut app, point);
assert_eq!(app.authoring_palette_tab, tab);
authoring_capture(&app, tab.widget_id());
let point = find_in_view(&app, row);
authoring_click(&mut app, point);
if tab == Tab::ParameterSets {
let mut simulator = iced_test::Simulator::with_size(
iced::Settings::default(),
VIEW_SIZE,
app.view_main(),
);
simulator.point_at(point);
simulator.simulate([iced::Event::Mouse(iced::mouse::Event::WheelScrolled {
delta: iced::mouse::ScrollDelta::Lines { x: 0.0, y: -100.0 },
})]);
simulator
.click("Lookup Set")
.expect("last set is reachable by scrolling");
authoring_snapshot(&mut simulator, &app.active_theme, "parameter-sets-bottom");
let messages = simulator.into_messages().collect::<Vec<_>>();
for message in messages {
let _ = app.update(message);
}
}
assert_eq!(
app.tabs[i]
.active_cmd
.as_ref()
.map(|command| command.name()),
Some("LINE")
);
}
assert_eq!(app.tabs[i].scene.document, before);
assert_eq!(app.tabs[i].history.undo_stack.len(), history);
assert!(!app.block_edit_dirty(i));
});
}
#[test]
fn authoring_palettes_constraints_start_the_existing_commands_from_real_rows() {
with_stack(|| {
use crate::ui::window::authoring_palettes::Tab;
let mut app = authoring_app();
let _ = app.dispatch_command("BAUTHORPALETTE");
let point = authoring_control(&app, Tab::Constraints.widget_id());
authoring_click(&mut app, point);
assert_eq!(app.authoring_palette_tab, Tab::Constraints);
authoring_capture(&app, Tab::Constraints.widget_id());
// The palette row is distinct from the ribbon's Coincident button.
let point = authoring_control(&app, "authoring-tool-CCONSTRAINT");
authoring_click(&mut app, point);
assert_eq!(
app.tabs[app.active_tab]
.active_cmd
.as_ref()
.map(|command| command.name()),
Some("GCCOINCIDENT")
);
let messages = {
let mut simulator = iced_test::Simulator::with_size(
iced::Settings::default(),
VIEW_SIZE,
app.view_main(),
);
simulator.point_at(point);
simulator.simulate([iced::Event::Mouse(iced::mouse::Event::WheelScrolled {
delta: iced::mouse::ScrollDelta::Lines { x: 0.0, y: -100.0 },
})]);
simulator
.click(iced_test::core::widget::Id::new(
"authoring-tool-DCDIAMETER",
))
.expect("last dimensional constraint is reachable by scrolling");
authoring_snapshot(&mut simulator, &app.active_theme, "constraints-bottom");
simulator.into_messages().collect::<Vec<_>>()
};
for message in messages {
let _ = app.update(message);
}
assert_eq!(
app.tabs[app.active_tab]
.active_cmd
.as_ref()
.map(|command| command.name()),
Some("DCDIAMETER")
);
assert!(!app.block_edit_dirty(app.active_tab));
});
}
#[test]
fn authoring_palettes_selected_page_survives_test_block_and_close_and_ignores_hidden_input() {
use crate::ui::window::authoring_palettes::Tab;
let mut app = authoring_app();
let _ = app.dispatch_command("BAUTHORPALETTE");
let _ = app.update(Message::AuthoringPaletteTabSelected(Tab::ParameterSets));
let _ = app.dispatch_command("BTESTBLOCK");
let _ = app.update(Message::AuthoringPaletteTabSelected(Tab::Constraints));
assert_eq!(app.authoring_palette_tab, Tab::ParameterSets);
let _ = app.dispatch_command("BCLOSETESTBLOCK");
assert_eq!(app.authoring_palette_tab, Tab::ParameterSets);
let _ = app.dispatch_command("BAUTHORPALETTECLOSE");
let _ = app.update(Message::AuthoringPaletteTabSelected(Tab::Actions));
assert_eq!(app.authoring_palette_tab, Tab::ParameterSets);
let _ = app.dispatch_command("BAUTHORPALETTE");
assert_eq!(app.authoring_palette_tab, Tab::ParameterSets);
}
/// Optional rendered evidence when reviewing palette layout changes.
fn authoring_capture(app: &OpenCADStudio, name: &str) {
if std::env::var_os("OCS_AUTHORING_PALETTE_CAPTURES").is_none() {
return;
}
let mut simulator =
iced_test::Simulator::with_size(iced::Settings::default(), VIEW_SIZE, app.view_main());
authoring_snapshot(&mut simulator, &app.active_theme, name);
}
fn authoring_snapshot(
simulator: &mut iced_test::Simulator<'_, Message>,
theme: &iced::Theme,
name: &str,
) {
let Some(directory) = std::env::var_os("OCS_AUTHORING_PALETTE_CAPTURES") else {
return;
};
let path = std::path::PathBuf::from(directory).join(format!("{name}.png"));
assert!(simulator
.snapshot(theme)
.expect("render palette")
.matches_image(path)
.expect("write palette evidence"));
}
#[test]
fn visibility_parameter_placement_starts_from_the_actual_palette_row() {
with_stack(|| {
let mut app = authoring_app();
let _ = app.dispatch_command("BAUTHORPALETTE");
let before = app.tabs[app.active_tab].scene.document.clone();
let point = authoring_control(&app, "authoring-tool-visibility");
authoring_click(&mut app, point);
assert_eq!(
app.tabs[app.active_tab].active_cmd.as_ref().unwrap().name(),
"BPARAMETER"
);
assert_eq!(app.tabs[app.active_tab].scene.document, before);
let _ = app.update(Message::CommandInput("0.25,0.25,0".into()));
let _ = app.update(Message::CommandSubmit);
assert!(app
.visibility_placement_error()
.unwrap()
.contains("already"));
let before = app.tabs[app.active_tab].scene.document.clone();
let _ = app.dispatch_command("LINE");
let point = authoring_control(&app, "authoring-tool-visibility");
authoring_click(&mut app, point);
assert_eq!(
app.tabs[app.active_tab].active_cmd.as_ref().unwrap().name(),
"LINE"
);
assert_eq!(app.tabs[app.active_tab].scene.document, before);
authoring_capture(&app, "visibility-parameter-placed");
});
}