concinnity-dev 0.19.119

The Concinnity dev tooling library: world authoring, the in-engine editor, the debug server, docs and packaging
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
//! The Behavior panel's keyboard (`hook/behavior_keys.rs`): the arrows stepping
//! the outline, the chart chain and the overview, Enter opening the palette and
//! picking from it, Escape unwinding one waiting state at a time, Tab cycling
//! the views, and the clipboard verbs. Each also asserts what the keys stand
//! down for: a focused field keeps them.

use concinnity_core::components::FrameInput;
use concinnity_core::components::InputKey;
use concinnity_core::components::KeyEvent;
use concinnity_core::components::KeyMods;
use concinnity_core::components::KeyPress;

use super::fixtures::{
    behavior, behavior_escape_input, behavior_session, open_args, press_behavior_key, press_remove,
    select_behavior, type_name,
};
use crate::editor::behavior;
use crate::editor::behavior::panel::{BehaviorAction, ViewMode};
use crate::editor::hook::EditorHook;

use crate::editor::panels::registry::PanelKey;
use crate::editor::widget;

// The title of the card the chart's selection belongs to.
fn selected_card_title(h: &EditorHook) -> Option<String> {
    let data = h.behavior_data();
    data.card
        .and_then(|i| data.chart.cards.get(i))
        .map(|c| c.title.clone())
}

fn selected_overview_title(h: &EditorHook) -> Option<String> {
    let data = h.behavior_data();
    h.behavior
        .overview_card
        .and_then(|i| data.overview.cards.get(i))
        .map(|c| c.title.clone())
}

// The outline is a list, so a step is one row. With nothing selected the first
// press starts from the end it comes from, and neither end wraps.
#[test]
fn behavior_arrows_step_the_outline_one_row_at_a_time() {
    let (mut h, mut world) = behavior_session(vec![behavior(
        "chase",
        serde_json::json!({"on": "tick", "do": [{"save": {}}, {"hide": {"target": "self"}}]}),
    )]);
    assert_eq!(h.behavior.row, None);

    press_behavior_key(&mut h, &mut world, InputKey::Down);
    assert_eq!(h.behavior.row, Some(0));
    press_behavior_key(&mut h, &mut world, InputKey::Down);
    assert_eq!(h.behavior.row, Some(1));
    press_behavior_key(&mut h, &mut world, InputKey::Up);
    assert_eq!(h.behavior.row, Some(0));
    press_behavior_key(&mut h, &mut world, InputKey::Up);
    assert_eq!(h.behavior.row, Some(0), "the top of the list does not wrap");

    // Left and Right have nothing to follow in a list.
    press_behavior_key(&mut h, &mut world, InputKey::Right);
    assert_eq!(h.behavior.row, Some(0));
}

// Stepping past the window scrolls it, so the selection is never off screen.
#[test]
fn behavior_arrows_scroll_the_outline_to_keep_the_selection_showing() {
    let body: Vec<serde_json::Value> = (0..30).map(|_| serde_json::json!({"save": {}})).collect();
    let (mut h, mut world) = behavior_session(vec![behavior(
        "long",
        serde_json::json!({"on": "start", "do": body}),
    )]);
    for _ in 0..25 {
        press_behavior_key(&mut h, &mut world, InputKey::Down);
    }
    let row = h.behavior.row.expect("a row is selected");
    assert_eq!(row, 24);
    assert!(h.behavior.scroll > 0, "the window followed the selection");
    assert!(row >= h.behavior.scroll, "row {row} is above the window");
}

// The chart is spatial: a sideways step follows the chain into a branch, and a
// vertical one crosses between the branches stacked under a branching node.
#[test]
fn behavior_arrows_follow_the_chart_chain_and_cross_its_branches() {
    let (mut h, mut world) = behavior_session(vec![behavior(
        "chase",
        serde_json::json!({"on": "tick", "do": [
            {"if": {
                "cond": {"bool": true},
                "then": [{"show": {"target": "self"}}],
                "else": [{"hide": {"target": "self"}}],
            }},
            {"save": {}},
        ]}),
    )]);
    h.apply_behavior_action(BehaviorAction::ToggleView, &mut world, [0.0, 0.0]);
    assert_eq!(h.behavior.mode, ViewMode::Chart);

    // With nothing selected the chart starts at its first card, the trigger.
    press_behavior_key(&mut h, &mut world, InputKey::Right);
    assert_eq!(selected_card_title(&h).as_deref(), Some("on tick"));
    press_behavior_key(&mut h, &mut world, InputKey::Right);
    assert_eq!(selected_card_title(&h).as_deref(), Some("if"));
    press_behavior_key(&mut h, &mut world, InputKey::Right);
    assert_eq!(selected_card_title(&h).as_deref(), Some("show"));

    press_behavior_key(&mut h, &mut world, InputKey::Down);
    assert_eq!(selected_card_title(&h).as_deref(), Some("hide"));
    press_behavior_key(&mut h, &mut world, InputKey::Up);
    assert_eq!(selected_card_title(&h).as_deref(), Some("show"));
    press_behavior_key(&mut h, &mut world, InputKey::Left);
    assert_eq!(selected_card_title(&h).as_deref(), Some("if"));
}

// The map opens on the behavior that was showing, steps between its cards, and
// Enter opens the behavior the card it lands on stands for.
#[test]
fn behavior_arrows_step_the_overview_and_enter_opens_a_behavior() {
    let (mut h, mut world) = behavior_session(vec![
        behavior(
            "award",
            serde_json::json!({"on": "start",
                "do": [{"set": {"var": "score", "value": {"int": 1}}}]}),
        ),
        behavior(
            "react",
            serde_json::json!({"on": {"variable": "score"}, "do": []}),
        ),
    ]);
    for _ in 0..2 {
        h.apply_behavior_action(BehaviorAction::ToggleView, &mut world, [0.0, 0.0]);
    }
    assert_eq!(h.behavior.mode, ViewMode::Overview);
    assert_eq!(
        selected_overview_title(&h).as_deref(),
        Some("award"),
        "the map opens on the behavior that was showing"
    );

    press_behavior_key(&mut h, &mut world, InputKey::Right);
    assert_eq!(selected_overview_title(&h).as_deref(), Some("score"));
    // Enter reaches what a card stands for, whatever that is: a variable card
    // opens the table declaring it and leaves the map showing.
    press_behavior_key(&mut h, &mut world, InputKey::Enter);
    assert_eq!(h.behavior.mode, ViewMode::Overview);
    assert!(
        h.open[PanelKey::Variables],
        "the table the variable is declared in"
    );

    press_behavior_key(&mut h, &mut world, InputKey::Right);
    assert_eq!(selected_overview_title(&h).as_deref(), Some("react"));
    press_behavior_key(&mut h, &mut world, InputKey::Enter);
    assert_eq!(h.behavior.index, 1);
    assert_eq!(h.behavior_data().name, "react");
    assert_eq!(h.behavior.mode, ViewMode::Chart);
}

// Reaching a card from the overview leaves the keyboard on it, so the arrows
// step on from what was just reached rather than from nothing.
#[test]
fn the_overview_keyboard_steps_on_from_the_card_last_reached() {
    let (mut h, mut world) = behavior_session(vec![
        serde_json::json!({"type": "Scene", "args": {"$id": "hub"}}),
        behavior(
            "finish",
            serde_json::json!({"on": "start", "do": [{"scene": {"scene": "hub"}}]}),
        ),
    ]);
    for _ in 0..2 {
        h.apply_behavior_action(BehaviorAction::ToggleView, &mut world, [0.0, 0.0]);
    }
    let hub = h
        .behavior_data()
        .overview
        .cards
        .iter()
        .position(|c| c.title == "hub")
        .expect("the scene the behavior sends the world to");
    h.apply_behavior_action(BehaviorAction::SelectAsset(hub), &mut world, [0.0, 0.0]);
    assert_eq!(selected_overview_title(&h).as_deref(), Some("hub"));

    press_behavior_key(&mut h, &mut world, InputKey::Left);
    assert_eq!(selected_overview_title(&h).as_deref(), Some("finish"));
}

// With no field focused, Enter opens the selected row's palette; the palette
// then takes the arrows, and Enter inserts what it is highlighting.
#[test]
fn behavior_enter_opens_the_palette_and_its_arrows_pick_from_it() {
    let (mut h, mut world) = behavior_session(vec![behavior(
        "chase",
        serde_json::json!({"on": "tick", "do": []}),
    )]);
    select_behavior(&mut h, &mut world, "do");
    assert!(!h.behavior.focus, "a list row takes no typed value");

    press_behavior_key(&mut h, &mut world, InputKey::Enter);
    assert!(h.behavior.picking, "Enter opened the palette");
    assert_eq!(h.behavior.pick, 0);

    let second = h.behavior_data().picks[1].verb;
    press_behavior_key(&mut h, &mut world, InputKey::Down);
    assert_eq!(h.behavior.pick, 1);
    press_behavior_key(&mut h, &mut world, InputKey::Enter);

    assert!(!h.behavior.picking, "picking closed the palette");
    let body = open_args(&h)["do"].clone();
    assert!(
        body[0].get(second).is_some(),
        "the highlighted option is the one that landed: {body:?}"
    );
}

// A row offering nothing has no palette, so Enter is left alone rather than
// arming one that never shows.
#[test]
fn behavior_enter_on_a_row_with_no_options_opens_nothing() {
    let (mut h, mut world) = behavior_session(vec![behavior(
        "chase",
        serde_json::json!({"on": "tick", "do": [{"let": {"name": "t", "value": {"int": 1}}}]}),
    )]);
    select_behavior(&mut h, &mut world, "name");
    assert!(h.behavior_data().picks.is_empty());
    press_behavior_key(&mut h, &mut world, InputKey::Enter);
    assert!(!h.behavior.picking);
}

// The highlight brings itself into the window, so a vocabulary longer than the
// palette shows is still reachable a press at a time.
#[test]
fn behavior_palette_highlight_scrolls_itself_into_the_window() {
    let (mut h, mut world) = behavior_session(vec![behavior(
        "chase",
        serde_json::json!({"on": "tick", "do": []}),
    )]);
    select_behavior(&mut h, &mut world, "do");
    press_behavior_key(&mut h, &mut world, InputKey::Enter);
    let total = h.behavior_data().picks.len();
    assert!(
        total > behavior::panel::PICK_POOL,
        "the node vocabulary overflows the palette"
    );

    for _ in 0..behavior::panel::PICK_POOL {
        press_behavior_key(&mut h, &mut world, InputKey::Down);
    }
    assert_eq!(h.behavior.pick, behavior::panel::PICK_POOL);
    assert!(h.behavior.pick_scroll > 0, "the window followed it down");
    assert!(h.behavior.pick >= h.behavior.pick_scroll);
    assert!(h.behavior.pick < h.behavior.pick_scroll + behavior::panel::PICK_POOL);

    // And back up again, dragging the window with it.
    for _ in 0..behavior::panel::PICK_POOL {
        press_behavior_key(&mut h, &mut world, InputKey::Up);
    }
    assert_eq!(h.behavior.pick, 0);
    assert_eq!(h.behavior.pick_scroll, 0);
}

// Escape answers whichever state is waiting on a press, most consequential
// first: the open palette, then an armed removal, then the focused field.
#[test]
fn behavior_escape_clears_one_waiting_state_at_a_time() {
    let (mut h, mut world) = behavior_session(vec![behavior(
        "chase",
        serde_json::json!({"on": "tick", "do": [{"let": {"name": "t", "value": {"int": 1}}}]}),
    )]);
    select_behavior(&mut h, &mut world, "do");
    press_behavior_key(&mut h, &mut world, InputKey::Enter);
    assert!(h.behavior.picking);
    h.behavior_keys(&mut world, &behavior_escape_input());
    assert!(!h.behavior.picking, "the palette closed without picking");
    assert_eq!(open_args(&h)["do"].as_array().map(Vec::len), Some(1));

    press_remove(&mut h, &mut world);
    assert!(h.behavior.remove_armed);
    h.behavior_keys(&mut world, &behavior_escape_input());
    assert!(!h.behavior.remove_armed, "the armed removal was canceled");
    assert_eq!(h.behavior_entries().len(), 1);

    select_behavior(&mut h, &mut world, "name");
    assert!(h.behavior.focus, "a text row takes the value field");
    h.behavior_keys(&mut world, &behavior_escape_input());
    assert!(!h.behavior.focus, "the value field gave the keyboard up");
}

// Escape gives the name field up without committing, reverting what was typed
// rather than leaving it to be committed by a later Enter.
#[test]
fn behavior_escape_reverts_an_abandoned_rename() {
    let (mut h, mut world) = behavior_session(vec![behavior(
        "chase",
        serde_json::json!({"on": "tick", "do": []}),
    )]);
    h.apply_behavior_action(BehaviorAction::FocusName, &mut world, [0.0, 0.0]);
    type_name(&mut world, "half typed");
    h.behavior_keys(&mut world, &behavior_escape_input());

    assert!(!h.behavior.name_focus);
    assert_eq!(h.behavior_data().name, "chase");
    assert_eq!(
        widget::field_text(&world, behavior::panel::NAME_INPUT),
        "chase",
    );
}

// Left and Right are the caret's while the value field holds the keyboard, so
// they never also move the selection; Up and Down are free to.
#[test]
fn behavior_horizontal_keys_stay_with_the_caret_while_a_value_is_focused() {
    let (mut h, mut world) = behavior_session(vec![behavior(
        "chase",
        serde_json::json!({"on": "tick", "do": [{"let": {"name": "t", "value": {"int": 1}}}]}),
    )]);
    select_behavior(&mut h, &mut world, "name");
    assert!(h.behavior.focus);
    let row = h.behavior.row;

    for key in [InputKey::Left, InputKey::Right] {
        press_behavior_key(&mut h, &mut world, key);
        assert_eq!(h.behavior.row, row, "{key:?} moved the selection");
    }
    press_behavior_key(&mut h, &mut world, InputKey::Down);
    assert_ne!(h.behavior.row, row, "Down still steps the outline");
}

// The name field is the asset's rather than the selection's, so it holds the
// arrows until Enter or Escape gives it up.
#[test]
fn behavior_name_field_holds_the_arrows_until_it_is_given_up() {
    let (mut h, mut world) = behavior_session(vec![behavior(
        "chase",
        serde_json::json!({"on": "tick", "do": [{"save": {}}]}),
    )]);
    h.apply_behavior_action(BehaviorAction::FocusName, &mut world, [0.0, 0.0]);
    press_behavior_key(&mut h, &mut world, InputKey::Down);
    assert_eq!(h.behavior.row, None, "the arrows did not reach the outline");

    h.behavior_keys(&mut world, &behavior_escape_input());
    press_behavior_key(&mut h, &mut world, InputKey::Down);
    assert_eq!(h.behavior.row, Some(0));
}

// Tab walks the same three-view cycle the header's button does, and the
// selection survives it, because all three views are over the one asset.
#[test]
fn behavior_tab_cycles_through_the_three_views() {
    let (mut h, mut world) = behavior_session(vec![behavior(
        "chase",
        serde_json::json!({"on": "tick", "do": [{"hide": {"target": "self"}}]}),
    )]);
    select_behavior(&mut h, &mut world, "hide");
    let selected = h.behavior.row;
    assert_eq!(h.behavior.mode, ViewMode::Outline);

    for want in [ViewMode::Chart, ViewMode::Overview, ViewMode::Outline] {
        press_behavior_key(&mut h, &mut world, InputKey::Tab);
        assert_eq!(h.behavior.mode, want);
        assert_eq!(
            h.behavior.row, selected,
            "the selection survives the switch"
        );
    }
}

// A half-typed rename is not a view switch: the name field holds Tab the same
// way it holds the arrows, until Enter or Escape gives the keyboard up.
#[test]
fn behavior_tab_leaves_the_view_alone_while_the_name_field_is_focused() {
    let (mut h, mut world) = behavior_session(vec![behavior(
        "chase",
        serde_json::json!({"on": "tick", "do": []}),
    )]);
    h.apply_behavior_action(BehaviorAction::FocusName, &mut world, [0.0, 0.0]);
    type_name(&mut world, "half typed");
    press_behavior_key(&mut h, &mut world, InputKey::Tab);
    assert_eq!(h.behavior.mode, ViewMode::Outline);
    assert!(h.behavior.name_focus, "the field kept the keyboard");

    h.behavior_keys(&mut world, &behavior_escape_input());
    press_behavior_key(&mut h, &mut world, InputKey::Tab);
    assert_eq!(h.behavior.mode, ViewMode::Chart);
}

// The open palette is modal, so Tab neither switches the view out from under it
// nor picks anything.
#[test]
fn behavior_tab_does_nothing_while_the_palette_is_open() {
    let (mut h, mut world) = behavior_session(vec![behavior(
        "chase",
        serde_json::json!({"on": "tick", "do": []}),
    )]);
    select_behavior(&mut h, &mut world, "do");
    press_behavior_key(&mut h, &mut world, InputKey::Enter);
    assert!(h.behavior.picking);

    press_behavior_key(&mut h, &mut world, InputKey::Tab);
    assert_eq!(h.behavior.mode, ViewMode::Outline);
    assert!(h.behavior.picking, "the palette is still up");
    assert_eq!(open_args(&h)["do"].as_array().map(Vec::len), Some(0));
}

fn ctrl_key_input(key: InputKey) -> FrameInput {
    FrameInput {
        key_events: vec![KeyEvent::Press(KeyPress::new(key, KeyMods::CTRL))],
        ctrl: true,
        viewport: [1280.0, 720.0],
        ..Default::default()
    }
}

fn body_verbs(h: &EditorHook) -> Vec<String> {
    open_args(h)["do"]
        .as_array()
        .map(|a| {
            a.iter()
                .filter_map(|n| n.as_object()?.keys().next().cloned())
                .collect()
        })
        .unwrap_or_default()
}

// Duplicating puts the copy beside the original, carrying its whole subtree, and
// leaves the selection on what just landed so a follow-up acts on the copy.
#[test]
fn behavior_duplicate_copies_a_node_subtree_next_to_it() {
    let (mut h, mut world) = behavior_session(vec![behavior(
        "chase",
        serde_json::json!({"on": "start", "do": [
            {"if": {"cond": {"bool": true}, "then": [{"hide": {"target": "self"}}]}},
            {"save": {}},
        ]}),
    )]);
    select_behavior(&mut h, &mut world, "if");
    h.apply_behavior_action(BehaviorAction::Duplicate, &mut world, [0.0, 0.0]);

    assert_eq!(body_verbs(&h), ["if", "if", "save"]);
    assert_eq!(
        open_args(&h)["do"][1]["if"]["then"][0]["hide"]["target"],
        serde_json::json!("self"),
        "the branch came with it"
    );
    let row = h.behavior.row.expect("the copy is selected");
    assert_eq!(
        h.behavior_rows()[row].element,
        Some(vec![
            crate::editor::behavior::path::field("do"),
            crate::editor::behavior::path::Step::Index(1),
        ]),
    );
}

// Ctrl+C then Ctrl+V is the same move spelled out, and what is held survives to
// be pasted again.
#[test]
fn behavior_ctrl_c_holds_a_node_and_ctrl_v_places_it() {
    let (mut h, mut world) = behavior_session(vec![behavior(
        "chase",
        serde_json::json!({"on": "start", "do": [{"save": {}}, {"hide": {"target": "self"}}]}),
    )]);
    select_behavior(&mut h, &mut world, "hide");
    h.behavior_keys(&mut world, &ctrl_key_input(InputKey::C));
    assert!(h.behavior.clip.is_some(), "the node is held");
    assert_eq!(body_verbs(&h), ["save", "hide"], "copying wrote nothing");

    h.behavior_keys(&mut world, &ctrl_key_input(InputKey::V));
    assert_eq!(body_verbs(&h), ["save", "hide", "hide"]);
    // Still held, so a second paste lands beside the first copy.
    h.behavior_keys(&mut world, &ctrl_key_input(InputKey::V));
    assert_eq!(body_verbs(&h), ["save", "hide", "hide", "hide"]);
}

// A duplicate is a paste of the selection, so it must not disturb what a Ctrl+C
// earlier put aside.
#[test]
fn behavior_duplicate_leaves_what_is_held_alone() {
    let (mut h, mut world) = behavior_session(vec![behavior(
        "chase",
        serde_json::json!({"on": "start", "do": [{"save": {}}, {"hide": {"target": "self"}}]}),
    )]);
    select_behavior(&mut h, &mut world, "save");
    h.behavior_keys(&mut world, &ctrl_key_input(InputKey::C));

    select_behavior(&mut h, &mut world, "hide");
    h.behavior_keys(&mut world, &ctrl_key_input(InputKey::D));
    assert_eq!(body_verbs(&h), ["save", "hide", "hide"]);

    // What was held is still the `save`, and pastes as one.
    h.behavior_keys(&mut world, &ctrl_key_input(InputKey::V));
    assert_eq!(body_verbs(&h), ["save", "hide", "hide", "save"]);
}

// Carrying a node between behaviors is most of why the clipboard outlives the one
// it came from.
#[test]
fn behavior_clipboard_carries_a_node_to_another_behavior() {
    let (mut h, mut world) = behavior_session(vec![
        behavior(
            "chase",
            serde_json::json!({"on": "start", "do": [{"hide": {"target": "self"}}]}),
        ),
        behavior("greet", serde_json::json!({"on": "tick", "do": []})),
    ]);
    select_behavior(&mut h, &mut world, "hide");
    h.behavior_keys(&mut world, &ctrl_key_input(InputKey::C));

    h.apply_behavior_action(BehaviorAction::Step(1), &mut world, [0.0, 0.0]);
    assert_eq!(h.behavior_data().name, "greet");
    assert!(h.behavior.clip.is_some(), "opening another kept it");

    // The empty body's own row is the list, so a paste there appends.
    select_behavior(&mut h, &mut world, "do");
    h.behavior_keys(&mut world, &ctrl_key_input(InputKey::V));
    assert_eq!(body_verbs(&h), ["hide"]);
}

// A node does not belong in a list of component names, so nothing is written and
// the world is left as it was.
#[test]
fn behavior_paste_is_refused_by_a_list_of_another_kind() {
    let (mut h, mut world) = behavior_session(vec![behavior(
        "chase",
        serde_json::json!({"on": "start", "scope": ["Prop"], "do": [{"save": {}}]}),
    )]);
    select_behavior(&mut h, &mut world, "save");
    h.behavior_keys(&mut world, &ctrl_key_input(InputKey::C));

    select_behavior(&mut h, &mut world, "scope");
    h.behavior_keys(&mut world, &ctrl_key_input(InputKey::V));
    assert_eq!(open_args(&h)["scope"], serde_json::json!(["Prop"]));
    assert_eq!(body_verbs(&h), ["save"], "and the body is untouched too");
}

// A row that is not a member of any list has nothing to carry.
#[test]
fn behavior_copy_of_a_non_member_holds_nothing() {
    let (mut h, mut world) = behavior_session(vec![behavior(
        "chase",
        serde_json::json!({"on": "start", "do": [{"save": {}}]}),
    )]);
    select_behavior(&mut h, &mut world, "on");
    h.behavior_keys(&mut world, &ctrl_key_input(InputKey::C));
    assert!(h.behavior.clip.is_none());
    h.behavior_keys(&mut world, &ctrl_key_input(InputKey::D));
    assert_eq!(
        body_verbs(&h),
        ["save"],
        "and there is nothing to duplicate"
    );
}

// The clipboard keys are about the selected node, so a field holding the keyboard
// keeps them: Ctrl+C while typing a value must not duplicate a node behind it.
#[test]
fn behavior_clipboard_keys_stand_down_while_a_field_is_focused() {
    let (mut h, mut world) = behavior_session(vec![behavior(
        "chase",
        serde_json::json!({"on": "start", "do": [{"let": {"name": "t", "value": {"int": 1}}}]}),
    )]);
    select_behavior(&mut h, &mut world, "name");
    assert!(h.behavior.focus, "a text row takes the value field");
    h.behavior_keys(&mut world, &ctrl_key_input(InputKey::D));
    assert_eq!(body_verbs(&h), ["let"], "nothing was duplicated");

    h.apply_behavior_action(BehaviorAction::FocusName, &mut world, [0.0, 0.0]);
    h.behavior_keys(&mut world, &ctrl_key_input(InputKey::D));
    assert_eq!(body_verbs(&h), ["let"]);
}

// The edit goes through the same commit every other one does, so it is undoable.
#[test]
fn behavior_duplicate_is_undoable() {
    let (mut h, mut world) = behavior_session(vec![behavior(
        "chase",
        serde_json::json!({"on": "start", "do": [{"save": {}}]}),
    )]);
    select_behavior(&mut h, &mut world, "save");
    h.apply_behavior_action(BehaviorAction::Duplicate, &mut world, [0.0, 0.0]);
    assert_eq!(body_verbs(&h), ["save", "save"]);
    h.undo(&mut world);
    assert_eq!(body_verbs(&h), ["save"]);
}