Verify Help preserves mixed-view caret and retires held Draw gestures
This commit is contained in:
@@ -721,3 +721,538 @@ async fn context_help_barrier_retires_text_selection_drag_without_changing_the_c
|
||||
"An unheld pointer no longer extends the pre-Help text selection"
|
||||
);
|
||||
}
|
||||
|
||||
// Union coverage: Help must own input before mixed-canvas draft/Draw admission.
|
||||
struct MixedHelp {
|
||||
app: App,
|
||||
diagram: Id,
|
||||
source: Id,
|
||||
target: Id,
|
||||
edge: Id,
|
||||
placement: Id,
|
||||
tool: crate::connection_tool::Tool,
|
||||
}
|
||||
|
||||
fn mixed_help(layer: Layer) -> MixedHelp {
|
||||
use crate::connection_tool::Tool;
|
||||
let mut p = Project::empty();
|
||||
let (kind, other, tool) = match layer {
|
||||
Layer::Oa => (Kind::Activity, Layer::Sa, Tool::Interaction),
|
||||
Layer::Sa => (Kind::Function, Layer::Oa, Tool::FunctionalExchange),
|
||||
};
|
||||
let source = p.add_element(kind, layer, "Send", None).unwrap();
|
||||
let target = p.add_element(kind, layer, "Receive", None).unwrap();
|
||||
p.ensure_diagram(layer);
|
||||
let diagram = p.ensure_diagram(other);
|
||||
p.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Traceability;
|
||||
let placement = p.place(diagram, source, Position { x: 20., y: 20. });
|
||||
p.place(diagram, target, Position { x: 400., y: 20. });
|
||||
let local_kind = if other == Layer::Oa {
|
||||
Kind::Activity
|
||||
} else {
|
||||
Kind::Function
|
||||
};
|
||||
let local = p
|
||||
.add_element(local_kind, other, "Other layer work", None)
|
||||
.unwrap();
|
||||
p.place(diagram, local, Position { x: 400., y: 240. });
|
||||
let edge_kind = if layer == Layer::Oa {
|
||||
RelationKind::Interaction
|
||||
} else {
|
||||
RelationKind::FunctionalExchange
|
||||
};
|
||||
let edge = p.relate(edge_kind, "Existing transfer", source, target);
|
||||
let mut app = App::with_project(p);
|
||||
app.page = Page::Diagram;
|
||||
app.layer = other;
|
||||
app.active_diagram = Some(diagram);
|
||||
app.selected = diagram;
|
||||
app.refresh_draft();
|
||||
app.reconcile_multiselect();
|
||||
choose(&mut app, placement, multiselect::Choice::Replace);
|
||||
assert_eq!(app.selected, source);
|
||||
assert_eq!((app.layer, app.active_diagram), (other, Some(diagram)));
|
||||
assert_ne!(app.selected, edge);
|
||||
assert!(
|
||||
project_view(app.project(), diagram)
|
||||
.edges
|
||||
.iter()
|
||||
.any(|visible| visible.id == edge)
|
||||
);
|
||||
MixedHelp {
|
||||
app,
|
||||
diagram,
|
||||
source,
|
||||
target,
|
||||
edge,
|
||||
placement,
|
||||
tool,
|
||||
}
|
||||
}
|
||||
|
||||
fn mixed_help_popup(f: &mut MixedHelp) -> Message {
|
||||
use crate::model_menu::{Event, Request};
|
||||
let mut request = Request::object(f.source, iced::Point::ORIGIN);
|
||||
request.canvas = f.app.route_context();
|
||||
request.placement = Some(f.placement);
|
||||
send(&mut f.app, Message::Menu(Event::Open(request)));
|
||||
let popup = crate::model_menu::popup(&f.app).expect("actual mixed-canvas popup");
|
||||
let label = format!("Draw {}", f.tool.label().to_lowercase());
|
||||
let action = popup
|
||||
.entries
|
||||
.into_iter()
|
||||
.find_map(|entry| match entry {
|
||||
crate::context_menu::Entry::Action {
|
||||
label: actual,
|
||||
message,
|
||||
..
|
||||
} if actual == label => message,
|
||||
_ => None,
|
||||
})
|
||||
.expect("visible opposite-layer source offers Draw");
|
||||
assert!(matches!(&action, Message::Menu(Event::Invoke(token, _)) if *token == popup.token));
|
||||
action
|
||||
}
|
||||
|
||||
fn mixed_help_workspace(app: &App) -> impl PartialEq + std::fmt::Debug + use<> {
|
||||
(
|
||||
(
|
||||
app.page,
|
||||
app.layer,
|
||||
app.active_diagram,
|
||||
app.selected,
|
||||
app.selected_node,
|
||||
),
|
||||
(app.zoom, app.pan),
|
||||
app.multiselect_snapshot().map(|s| s.targets().to_vec()),
|
||||
serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(),
|
||||
(app.source_editor.text(), app.source_version),
|
||||
(
|
||||
app.busy,
|
||||
app.file_prompt.is_some(),
|
||||
app.pending.is_some(),
|
||||
app.workflow.is_some(),
|
||||
),
|
||||
app.model_menu.as_ref().map(|m| m.token),
|
||||
)
|
||||
}
|
||||
|
||||
fn mixed_help_state(app: &App) -> impl PartialEq + std::fmt::Debug + use<> {
|
||||
(
|
||||
mixed_help_workspace(app),
|
||||
app.connection
|
||||
.as_ref()
|
||||
.map(|s| (s.tool, s.source, s.context, s.candidates.clone())),
|
||||
)
|
||||
}
|
||||
|
||||
fn mixed_help_document(
|
||||
app: &App,
|
||||
before: &oa_core::source_project::CompiledSourceProject,
|
||||
revision: u64,
|
||||
) {
|
||||
assert_eq!(app.project(), &before.decoded.project);
|
||||
assert_eq!(app.document.bundle(), &before.bundle);
|
||||
assert_eq!(app.document.revision(), revision);
|
||||
assert!(!app.document.can_undo());
|
||||
assert!(!app.document.can_redo());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed_context_help_blocks_navigation_draft_draw_and_save_before_fast_paths() {
|
||||
use crate::connection_edit::Event as Draw;
|
||||
use crate::model_menu::{Event as Menu, Request};
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
for draft in 0..3 {
|
||||
let mut f = mixed_help(layer);
|
||||
let queued_popup = mixed_help_popup(&mut f);
|
||||
if draft == 0 {
|
||||
send(&mut f.app, queued_popup.clone());
|
||||
let session = f
|
||||
.app
|
||||
.connection
|
||||
.as_ref()
|
||||
.expect("clean row has an admitted Draw session");
|
||||
assert_eq!((session.tool, session.source), (f.tool, Some(f.source)));
|
||||
assert!(session.candidates.contains(&f.target));
|
||||
} else {
|
||||
send(&mut f.app, Message::Menu(Menu::Close));
|
||||
}
|
||||
match draft {
|
||||
1 => send(
|
||||
&mut f.app,
|
||||
Message::Field("/name".into(), "Pending mixed name".into()),
|
||||
),
|
||||
2 => send(
|
||||
&mut f.app,
|
||||
Message::Field("/alias".into(), "invalid alias".into()),
|
||||
),
|
||||
_ => {}
|
||||
}
|
||||
assert_eq!(f.app.draft_dirty(), draft != 0);
|
||||
assert_eq!(f.app.connection.is_some(), draft == 0);
|
||||
let command = f.app.draft.as_ref().unwrap().command().unwrap();
|
||||
assert_eq!(
|
||||
oa_core::prepare_command(f.app.project(), &command).is_err(),
|
||||
draft == 2
|
||||
);
|
||||
let before = f.app.document.snapshot();
|
||||
let revision = f.app.document.revision();
|
||||
let original = mixed_help_state(&f.app);
|
||||
send(&mut f.app, Message::Help);
|
||||
assert!(f.app.context_help);
|
||||
assert_eq!(mixed_help_state(&f.app), original);
|
||||
let current = f.app.route_context().unwrap();
|
||||
let mut stale = current;
|
||||
stale.epoch = stale.epoch.wrapping_add(1);
|
||||
let mut request = Request::object(f.target, iced::Point::ORIGIN);
|
||||
request.canvas = Some(current);
|
||||
for message in [
|
||||
Message::CanvasSelect(current, f.edge),
|
||||
Message::CanvasSelect(stale, f.edge),
|
||||
Message::Menu(Menu::Open(request)),
|
||||
queued_popup,
|
||||
Message::Connection(Draw::Endpoint(current, f.target)),
|
||||
Message::Connection(Draw::Endpoint(stale, f.target)),
|
||||
Message::Connection(Draw::Restart(current)),
|
||||
Message::Connection(Draw::Restart(stale)),
|
||||
Message::Save(false),
|
||||
Message::Save(true),
|
||||
Message::Apply,
|
||||
Message::Undo,
|
||||
] {
|
||||
assert!(
|
||||
iced_runtime::task::into_stream(f.app.update(message)).is_none(),
|
||||
"blocked input must not schedule I/O or widget work"
|
||||
);
|
||||
assert!(f.app.context_help);
|
||||
assert_eq!(mixed_help_state(&f.app), original);
|
||||
mixed_help_document(&f.app, &before, revision);
|
||||
}
|
||||
send(&mut f.app, Message::CloseContextHelp);
|
||||
assert!(!f.app.context_help);
|
||||
assert_eq!(
|
||||
mixed_help_state(&f.app),
|
||||
original,
|
||||
"closing cannot replay queued input"
|
||||
);
|
||||
mixed_help_document(&f.app, &before, revision);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn mixed_help_click_close(
|
||||
mounted: &mut crate::behavior::canvas_workspace::tests::Mounted,
|
||||
app: &mut App,
|
||||
) {
|
||||
use iced::{advanced::widget, mouse};
|
||||
// Observe real header containers and require the actual button's message.
|
||||
// No direct Close dispatch masquerades as pointer/focus coverage.
|
||||
#[derive(Default)]
|
||||
struct Bounds(Vec<iced::Rectangle>);
|
||||
impl widget::Operation for Bounds {
|
||||
fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn widget::Operation)) {
|
||||
visit(self);
|
||||
}
|
||||
fn container(&mut self, _: Option<&widget::Id>, bounds: iced::Rectangle) {
|
||||
self.0.push(bounds);
|
||||
}
|
||||
}
|
||||
let article =
|
||||
mounted.geometry(app).scrolls[&widget::Id::new(crate::guide::ARTICLE_SCROLL_ID)].0;
|
||||
let mut bounds = Bounds::default();
|
||||
mounted.operate(app, &mut bounds);
|
||||
for rect in bounds.0.into_iter().filter(|r| {
|
||||
r.width < 180.
|
||||
&& r.height < 60.
|
||||
&& r.x > article.x + article.width - 180.
|
||||
&& r.y < article.y
|
||||
&& r.y > article.y - 80.
|
||||
}) {
|
||||
let at = rect.center();
|
||||
let messages = mounted.input(
|
||||
app,
|
||||
&[
|
||||
iced::Event::Mouse(mouse::Event::CursorMoved { position: at }),
|
||||
iced::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
|
||||
iced::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)),
|
||||
],
|
||||
mouse::Cursor::Available(at),
|
||||
);
|
||||
assert!(
|
||||
messages
|
||||
.iter()
|
||||
.all(|m| matches!(m, Message::CloseContextHelp)),
|
||||
"a candidate Close click must not leak another workspace command: {messages:?}"
|
||||
);
|
||||
if !messages.is_empty() {
|
||||
assert_eq!(
|
||||
messages.len(),
|
||||
1,
|
||||
"one pointer click emits exactly one Close"
|
||||
);
|
||||
let task = app.update(messages.into_iter().next().unwrap());
|
||||
mounted.perform(app, task).await;
|
||||
return;
|
||||
}
|
||||
}
|
||||
panic!("mounted Help Close button did not emit CloseContextHelp");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mounted_mixed_context_help_preserves_name_and_invalid_alias_carets_then_applies_once() {
|
||||
use crate::behavior::canvas_workspace::tests::{Mounted, character, key};
|
||||
use iced::keyboard::key::Named;
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
for invalid in [false, true] {
|
||||
for escape in [false, true] {
|
||||
let mut f = mixed_help(layer);
|
||||
let before = f.app.document.snapshot();
|
||||
let revision = f.app.document.revision();
|
||||
let mut mounted = Mounted::new(&mut f.app).await;
|
||||
let mut inputs: Vec<_> = mounted
|
||||
.geometry(&f.app)
|
||||
.text_inputs
|
||||
.into_iter()
|
||||
.filter(|b| b.x > f.app.window_size.width - 320.)
|
||||
.collect();
|
||||
inputs.sort_by(|a, b| a.y.total_cmp(&b.y));
|
||||
// Production forms render Name then Alias; assert the resulting raw field.
|
||||
let path = if invalid { "/alias" } else { "/name" };
|
||||
let old = f.app.draft.as_ref().unwrap().inputs[path].raw.clone();
|
||||
mounted
|
||||
.click(&mut f.app, inputs[usize::from(invalid)].center())
|
||||
.await;
|
||||
help_key(&mut mounted, &mut f.app, key(Named::Home)).await;
|
||||
let prefix = if invalid { "bad alias " } else { "DRAFT " };
|
||||
for value in prefix.chars() {
|
||||
help_key(&mut mounted, &mut f.app, character(&value.to_string())).await;
|
||||
}
|
||||
help_key(&mut mounted, &mut f.app, key(Named::ArrowLeft)).await;
|
||||
let raw = format!("{prefix}{old}");
|
||||
assert_eq!(f.app.draft.as_ref().unwrap().inputs[path].raw, raw);
|
||||
let command = f.app.draft.as_ref().unwrap().command().unwrap();
|
||||
assert_eq!(
|
||||
oa_core::prepare_command(f.app.project(), &command).is_err(),
|
||||
invalid
|
||||
);
|
||||
let state = mixed_help_state(&f.app);
|
||||
help_key(&mut mounted, &mut f.app, key(Named::F1)).await;
|
||||
assert!(f.app.context_help);
|
||||
let article = iced::advanced::widget::Id::new(crate::guide::ARTICLE_SCROLL_ID);
|
||||
help_key(&mut mounted, &mut f.app, key(Named::PageDown)).await;
|
||||
assert!(mounted.geometry(&f.app).scrolls[&article].1.y > 0.);
|
||||
help_key(&mut mounted, &mut f.app, key(Named::End)).await;
|
||||
help_key(&mut mounted, &mut f.app, character("IGNORED")).await;
|
||||
let messages = mounted
|
||||
.batch(
|
||||
&mut f.app,
|
||||
&[iced::Event::InputMethod(
|
||||
iced::advanced::input_method::Event::Commit("雨".into()),
|
||||
)],
|
||||
iced::mouse::Cursor::Unavailable,
|
||||
)
|
||||
.await;
|
||||
assert!(!messages.iter().any(|m| matches!(m, Message::Field(..))));
|
||||
assert_eq!(mixed_help_state(&f.app), state);
|
||||
mixed_help_document(&f.app, &before, revision);
|
||||
if escape {
|
||||
help_key(&mut mounted, &mut f.app, key(Named::Escape)).await;
|
||||
} else {
|
||||
mixed_help_click_close(&mut mounted, &mut f.app).await;
|
||||
}
|
||||
assert!(!f.app.context_help);
|
||||
assert_eq!(mixed_help_state(&f.app), state);
|
||||
help_key(&mut mounted, &mut f.app, character("X")).await;
|
||||
let expected_raw = format!("{}X {old}", prefix.trim_end());
|
||||
assert_eq!(
|
||||
f.app.draft.as_ref().unwrap().inputs[path].raw,
|
||||
expected_raw,
|
||||
"exact mid-string caret survives without refocusing"
|
||||
);
|
||||
let surface = (
|
||||
f.app.page,
|
||||
f.app.layer,
|
||||
f.app.active_diagram,
|
||||
f.app.selected,
|
||||
f.app.zoom,
|
||||
f.app.pan,
|
||||
);
|
||||
if invalid {
|
||||
let recovery =
|
||||
serde_json::to_value(f.app.draft.as_ref().unwrap().recovery_snapshot())
|
||||
.unwrap();
|
||||
assert!(!f.app.apply());
|
||||
assert_eq!(
|
||||
serde_json::to_value(f.app.draft.as_ref().unwrap().recovery_snapshot())
|
||||
.unwrap(),
|
||||
recovery
|
||||
);
|
||||
mixed_help_document(&f.app, &before, revision);
|
||||
assert_eq!(
|
||||
(
|
||||
f.app.page,
|
||||
f.app.layer,
|
||||
f.app.active_diagram,
|
||||
f.app.selected,
|
||||
f.app.zoom,
|
||||
f.app.pan
|
||||
),
|
||||
surface
|
||||
);
|
||||
} else {
|
||||
let mut expected = Document::from_bundle(
|
||||
before.bundle.clone(),
|
||||
oa_core::source_project::ProjectLimits::default(),
|
||||
)
|
||||
.unwrap();
|
||||
expected
|
||||
.execute(Command::Rename(f.source, expected_raw.clone()))
|
||||
.unwrap();
|
||||
let mut expected_project = before.decoded.project.clone();
|
||||
expected_project.elements.get_mut(&f.source).unwrap().name = expected_raw;
|
||||
assert!(f.app.apply());
|
||||
assert_eq!(f.app.project(), &expected_project);
|
||||
assert_eq!(f.app.document.bundle(), expected.bundle());
|
||||
assert_eq!(f.app.document.revision(), revision + 1);
|
||||
assert!(!f.app.document.can_redo());
|
||||
assert_eq!(
|
||||
(
|
||||
f.app.page,
|
||||
f.app.layer,
|
||||
f.app.active_diagram,
|
||||
f.app.selected,
|
||||
f.app.zoom,
|
||||
f.app.pan
|
||||
),
|
||||
surface
|
||||
);
|
||||
send(&mut f.app, Message::Undo);
|
||||
assert_eq!(f.app.project(), &before.decoded.project);
|
||||
assert_eq!(f.app.document.bundle(), &before.bundle);
|
||||
assert!(!f.app.document.can_undo());
|
||||
assert!(f.app.document.can_redo());
|
||||
send(&mut f.app, Message::Redo);
|
||||
assert_eq!(f.app.project(), &expected_project);
|
||||
assert_eq!(f.app.document.bundle(), expected.bundle());
|
||||
assert!(!f.app.document.can_redo());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mounted_mixed_context_help_preserves_draw_session_but_retires_held_release() {
|
||||
use crate::behavior::canvas_workspace::tests::{Mounted, key};
|
||||
use crate::connection_edit::Event as Draw;
|
||||
use iced::{keyboard::key::Named, mouse};
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut f = mixed_help(layer);
|
||||
f.app.window_size = iced::Size::new(2000., 1400.);
|
||||
f.app.zoom = 1.;
|
||||
f.app.pan = iced::Vector::ZERO;
|
||||
f.app.fit_to_view = false;
|
||||
let mut mounted = Mounted::new(&mut f.app).await;
|
||||
let before = f.app.document.snapshot();
|
||||
let revision = f.app.document.revision();
|
||||
let popup = mixed_help_popup(&mut f);
|
||||
send(&mut f.app, popup);
|
||||
let session = f.app.connection.as_ref().expect("actual popup armed Draw");
|
||||
assert_eq!((session.tool, session.source), (f.tool, Some(f.source)));
|
||||
assert!(session.candidates.contains(&f.target));
|
||||
let context = session.context;
|
||||
send(&mut f.app, Message::Connection(Draw::Restart(context)));
|
||||
let session = f.app.connection.as_ref().unwrap();
|
||||
assert_eq!((session.tool, session.source), (f.tool, None));
|
||||
let context = session.context;
|
||||
let bounds = mounted
|
||||
.geometry(&f.app)
|
||||
.bounds(crate::allocation_input::CANVAS_ID);
|
||||
let view = project_view(f.app.project(), f.diagram);
|
||||
let point = |id| {
|
||||
let node = view.nodes.iter().find(|node| node.element == id).unwrap();
|
||||
let point = iced::Point::new(
|
||||
bounds.x + (node.position.x + node.size.width / 2.) * f.app.zoom + f.app.pan.x,
|
||||
bounds.y + (node.position.y + node.size.height / 2.) * f.app.zoom + f.app.pan.y,
|
||||
);
|
||||
assert!(bounds.contains(point));
|
||||
point
|
||||
};
|
||||
let source = point(f.source);
|
||||
let target = point(f.target);
|
||||
let messages = mounted
|
||||
.batch(
|
||||
&mut f.app,
|
||||
&[
|
||||
iced::Event::Mouse(mouse::Event::CursorMoved { position: source }),
|
||||
iced::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
|
||||
],
|
||||
mouse::Cursor::Available(source),
|
||||
)
|
||||
.await;
|
||||
assert!(messages.iter().any(|m| matches!(m, Message::Connection(Draw::Endpoint(c, id)) if *c == context && *id == f.source)));
|
||||
let session = f.app.connection.as_ref().unwrap();
|
||||
assert_eq!((session.tool, session.source), (f.tool, Some(f.source)));
|
||||
assert!(session.candidates.contains(&f.target));
|
||||
let context = session.context;
|
||||
// The admitted multi-click tool/source is reading state. Only the held
|
||||
// widget gesture is retired; a later deliberate click is still valid.
|
||||
let state = mixed_help_state(&f.app);
|
||||
mixed_help_document(&f.app, &before, revision);
|
||||
help_key(&mut mounted, &mut f.app, key(Named::F1)).await;
|
||||
assert!(f.app.context_help);
|
||||
assert_eq!(mixed_help_state(&f.app), state);
|
||||
for message in [
|
||||
Message::Connection(Draw::Endpoint(context, f.target)),
|
||||
Message::Connection(Draw::Restart(context)),
|
||||
] {
|
||||
assert!(iced_runtime::task::into_stream(f.app.update(message)).is_none());
|
||||
assert_eq!(mixed_help_state(&f.app), state);
|
||||
mixed_help_document(&f.app, &before, revision);
|
||||
}
|
||||
let release = [
|
||||
iced::Event::Mouse(mouse::Event::CursorMoved { position: target }),
|
||||
iced::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)),
|
||||
];
|
||||
let messages = mounted
|
||||
.batch(&mut f.app, &release, mouse::Cursor::Available(target))
|
||||
.await;
|
||||
assert!(!messages.iter().any(|m| matches!(m, Message::Connection(_))));
|
||||
assert_eq!(mixed_help_state(&f.app), state);
|
||||
mixed_help_document(&f.app, &before, revision);
|
||||
help_key(&mut mounted, &mut f.app, key(Named::Escape)).await;
|
||||
assert!(!f.app.context_help);
|
||||
let messages = mounted
|
||||
.batch(&mut f.app, &release, mouse::Cursor::Available(target))
|
||||
.await;
|
||||
assert!(
|
||||
!messages.iter().any(|m| matches!(m, Message::Connection(_))),
|
||||
"a release after closing Help cannot resume the held press"
|
||||
);
|
||||
assert_eq!(mixed_help_state(&f.app), state);
|
||||
mixed_help_document(&f.app, &before, revision);
|
||||
// Observe the real fresh-click message without executing a randomly
|
||||
// identified creation; existing connection tests own its semantic delta.
|
||||
let current = f.app.route_context().unwrap();
|
||||
let messages = mounted.input(
|
||||
&f.app,
|
||||
&[
|
||||
iced::Event::Mouse(mouse::Event::CursorMoved { position: target }),
|
||||
iced::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
|
||||
iced::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)),
|
||||
],
|
||||
mouse::Cursor::Available(target),
|
||||
);
|
||||
let connections: Vec<_> = messages
|
||||
.iter()
|
||||
.filter(|m| matches!(m, Message::Connection(_)))
|
||||
.collect();
|
||||
assert_eq!(connections.len(), 1);
|
||||
assert!(
|
||||
matches!(connections[0], Message::Connection(Draw::Endpoint(c, id)) if *c == current && *id == f.target)
|
||||
);
|
||||
assert_eq!(mixed_help_state(&f.app), state);
|
||||
mixed_help_document(&f.app, &before, revision);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user