| author | |
| committer | |
| log | 3b673de430e5bec20ebf20c4ba79dd1572469f7e |
| tree | dfda5f98ed343f101e2cc6e35f57b609b8e38d8c |
| parent | 1407178c794c7688ff663f46095cb7c4eb24276f |
| signature | Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU (~clover) |
Share the sidebar and welcome menu, routing filesystem, SMB and Live Share choices through the existing opening flows. Preserve keyboard focus and disabled platform choices, with real accessibility-action replay coverage.
fixes #130
Assisted-by: gpt-6.1-sol3 files changed, 299 insertions(+), 106 deletions(-)
crates/snowbound/src/main.rs+36-1| ... | ... | @@ -390,6 +390,7 @@ impl std::task::Wake for Redraw { |
| 390 | 390 | #[derive(Debug)] |
| 391 | 391 | enum Replay { |
| 392 | 392 | Input(ui::Event), |
| 393 | Access(accesskit::Action, String), | |
| 393 | 394 | /// A trackpad pinch scaling the page by the factor. |
| 394 | 395 | Pinch(f32), |
| 395 | 396 | /// Paints the next frame into a PNG as well as the window. |
| ... | ... | @@ -6408,6 +6409,31 @@ impl State { |
| 6408 | 6409 | }; |
| 6409 | 6410 | match replay { |
| 6410 | 6411 | Replay::Input(event) => self.input(event), |
| 6412 | Replay::Access(action, label) => { | |
| 6413 | let tree = self.ui.accessibility_tree("", self.window.scale_factor()); | |
| 6414 | let mut nodes = tree | |
| 6415 | .nodes | |
| 6416 | .iter() | |
| 6417 | .filter(|(_, node)| node.label() == Some(label.as_str())); | |
| 6418 | let (id, node) = nodes | |
| 6419 | .next() | |
| 6420 | .unwrap_or_else(|| panic!("Replay label {label:?} is missing")); | |
| 6421 | assert!( | |
| 6422 | nodes.next().is_none(), | |
| 6423 | "Replay label {label:?} is ambiguous" | |
| 6424 | ); | |
| 6425 | assert!( | |
| 6426 | node.supports_action(action), | |
| 6427 | "Replay label {label:?} does not support {action:?}" | |
| 6428 | ); | |
| 6429 | self.access_action(accesskit::ActionRequest { | |
| 6430 | action, | |
| 6431 | target_tree: accesskit::TreeId::ROOT, | |
| 6432 | target_node: *id, | |
| 6433 | data: None, | |
| 6434 | }) | |
| 6435 | .expect("Replay accessibility action"); | |
| 6436 | } | |
| 6411 | 6437 | Replay::Pinch(factor) => { |
| 6412 | 6438 | if let Err(error) = self.pinch(factor) { |
| 6413 | 6439 | eprintln!("{error}"); |
| ... | ... | @@ -6794,7 +6820,7 @@ fn write_png(path: &Path, size: [u32; 2], pixels: &[u8]) -> Result<(), Box<dyn E |
| 6794 | 6820 | /// `pressure LEVEL|none`, `pinch FACTOR`, `key NAME`, `type TEXT`, `modifiers [shift] |
| 6795 | 6821 | /// [control] [command]`, `wait MILLISECONDS`, `snapshot PNG_PATH`, `settle` and |
| 6796 | 6822 | /// `accessibility TEXT_PATH` (which wait for what is on its way), `appearance light|dark`, |
| 6797 | /// `resize WIDTH HEIGHT` and `quit`. | |
| 6823 | /// `access click|focus LABEL`, `resize WIDTH HEIGHT` and `quit`. | |
| 6798 | 6824 | #[cfg(not(target_arch = "wasm32"))] |
| 6799 | 6825 | fn replay(script: String, proxy: EventLoopProxy<UserEvent>) -> Result<(), Box<dyn Error>> { |
| 6800 | 6826 | let (settle, settled) = std::sync::mpsc::channel(); |
| ... | ... | @@ -6868,6 +6894,15 @@ fn replay(script: String, proxy: EventLoopProxy<UserEvent>) -> Result<(), Box<dy |
| 6868 | 6894 | "snapshot" => Ok(Replay::Snapshot(rest.into())), |
| 6869 | 6895 | "settle" => Ok(Replay::Settle(None, settle.clone())), |
| 6870 | 6896 | "accessibility" => Ok(Replay::Settle(Some(rest.into()), settle.clone())), |
| 6897 | "access" => { | |
| 6898 | let (action, label) = rest.split_once(' ').ok_or("access takes ACTION LABEL")?; | |
| 6899 | let action = match action { | |
| 6900 | "click" => accesskit::Action::Click, | |
| 6901 | "focus" => accesskit::Action::Focus, | |
| 6902 | _ => return Err(format!("Unknown accessibility action: {action}").into()), | |
| 6903 | }; | |
| 6904 | Ok(Replay::Access(action, label.to_owned())) | |
| 6905 | } | |
| 6871 | 6906 | "renderer" => Ok(Replay::Renderer(settings::Backend::named(rest)?)), |
| 6872 | 6907 | "appearance" => Ok(Replay::Appearance(match rest { |
| 6873 | 6908 | "light" => winit::window::Theme::Light, |
crates/snowbound/src/sidebar.rs+129-76| ... | ... | @@ -29,7 +29,7 @@ pub const NAV: f32 = 2.0 * ui::shell::TOOL + 1.0 + (RAIL - ui::shell::TOOL); |
| 29 | 29 | const INDENT: f32 = 16.0; |
| 30 | 30 | |
| 31 | 31 | /// What the sidebar was asked to do. |
| 32 | pub enum Action { | |
| 32 | enum Action { | |
| 33 | 33 | /// Opens a notebook's section by catalog path. |
| 34 | 34 | Open { |
| 35 | 35 | notebook: usize, |
| ... | ... | @@ -37,10 +37,7 @@ pub enum Action { |
| 37 | 37 | }, |
| 38 | 38 | /// Folds or unfolds a notebook's or section group's rows, by `Library::key`. |
| 39 | 39 | Fold(String), |
| 40 | NewNotebook, | |
| 41 | OpenNotebook, | |
| 42 | #[cfg(feature = "live")] | |
| 43 | OpenShared, | |
| 40 | Command(crate::Command), | |
| 44 | 41 | /// Signs in again to the notebook opened from its server at this location. |
| 45 | 42 | SignIn(String), |
| 46 | 43 | Options, |
| ... | ... | @@ -56,7 +53,7 @@ pub enum Action { |
| 56 | 53 | } |
| 57 | 54 | |
| 58 | 55 | /// What the tree's rows read and what they were asked. |
| 59 | pub struct Tree<'a> { | |
| 56 | struct Tree<'a> { | |
| 60 | 57 | theme: &'a Theme, |
| 61 | 58 | /// The open section's notebook index and catalog path. |
| 62 | 59 | open: Option<(usize, &'a str)>, |
| ... | ... | @@ -331,34 +328,8 @@ fn sidebar( |
| 331 | 328 | ..Spec::default() |
| 332 | 329 | }, |
| 333 | 330 | ); |
| 334 | for (part, icon, label, chosen) in [ | |
| 335 | ( | |
| 336 | "new", | |
| 337 | Leading::Icon(art::PLUS), | |
| 338 | "New Notebook", | |
| 339 | Action::NewNotebook, | |
| 340 | ), | |
| 341 | ( | |
| 342 | "open", | |
| 343 | Leading::Notebook(art::NOTEBOOK, None), | |
| 344 | "Open Notebook", | |
| 345 | Action::OpenNotebook, | |
| 346 | ), | |
| 347 | #[cfg(feature = "live")] | |
| 348 | ( | |
| 349 | "shared", | |
| 350 | Leading::Icon(art::OPEN_SHARED), | |
| 351 | "Open Shared", | |
| 352 | Action::OpenShared, | |
| 353 | ), | |
| 354 | ( | |
| 355 | "options", | |
| 356 | Leading::Icon(art::OPTIONS), | |
| 357 | "Options", | |
| 358 | Action::Options, | |
| 359 | ), | |
| 360 | ] { | |
| 361 | let (row, _) = tree_row( | |
| 331 | let row = |ui: &mut Ui, tree: &mut Tree, part, icon, label| { | |
| 332 | tree_row( | |
| 362 | 333 | ui, |
| 363 | 334 | tree, |
| 364 | 335 | ui.id(part), |
| ... | ... | @@ -372,10 +343,27 @@ fn sidebar( |
| 372 | 343 | renamed: false, |
| 373 | 344 | unread: false, |
| 374 | 345 | }, |
| 375 | ); | |
| 376 | if row.clicked { | |
| 377 | tree.action = Some(chosen); | |
| 378 | } | |
| 346 | ) | |
| 347 | .0 | |
| 348 | }; | |
| 349 | if row(ui, tree, "new", Leading::Icon(art::PLUS), "New Notebook").clicked { | |
| 350 | tree.action = Some(Action::Command(crate::Command::NewNotebook)); | |
| 351 | } | |
| 352 | if let Some(command) = open_notebook( | |
| 353 | ui, | |
| 354 | Spec { | |
| 355 | size: [fill(), px(ROW)], | |
| 356 | hover_fill: Some(ui::mix(theme.sidebar, theme.hover(), 0.6)), | |
| 357 | radius: 4.0, | |
| 358 | pad: [ROW_PAD, 0.0], | |
| 359 | gap: ROW_PAD, | |
| 360 | ..Spec::default() | |
| 361 | }, | |
| 362 | ) { | |
| 363 | tree.action = Some(Action::Command(command)); | |
| 364 | } | |
| 365 | if row(ui, tree, "options", Leading::Icon(art::OPTIONS), "Options").clicked { | |
| 366 | tree.action = Some(Action::Options); | |
| 379 | 367 | } |
| 380 | 368 | ui.close(); |
| 381 | 369 | } |
| ... | ... | @@ -839,6 +827,77 @@ fn tree_row(ui: &mut Ui, tree: &mut Tree, id: Id, row: Row) -> (Signal, bool) { |
| 839 | 827 | (ui.signal(id), folded) |
| 840 | 828 | } |
| 841 | 829 | |
| 830 | fn open_notebook(ui: &mut Ui, spec: Spec) -> Option<crate::Command> { | |
| 831 | let button = ui.id("open"); | |
| 832 | let menu = button.child("menu"); | |
| 833 | let center = spec.center; | |
| 834 | let theme = ui.theme.clone(); | |
| 835 | ui.open_as( | |
| 836 | button, | |
| 837 | Spec { | |
| 838 | flags: Flags::CLICKABLE, | |
| 839 | role: Some(accesskit::Role::Button), | |
| 840 | ..spec | |
| 841 | }, | |
| 842 | ); | |
| 843 | ui.leaf( | |
| 844 | "label", | |
| 845 | Spec { | |
| 846 | size: [fill(), fill()], | |
| 847 | icon: Some(art::NOTEBOOK), | |
| 848 | tint: Some(crate::notebook_color(&theme, None)), | |
| 849 | text: Some(crate::commands::command(crate::commands::Id::OpenNotebook).title), | |
| 850 | center, | |
| 851 | ..Spec::default() | |
| 852 | }, | |
| 853 | ); | |
| 854 | ui.leaf( | |
| 855 | "arrow", | |
| 856 | Spec { | |
| 857 | size: [px(ICON), fill()], | |
| 858 | icon: Some(ui::shell::CHEVRON), | |
| 859 | color: Some(theme.text_dim), | |
| 860 | ..Spec::default() | |
| 861 | }, | |
| 862 | ); | |
| 863 | ui.close(); | |
| 864 | if ui.signal(button).pressed { | |
| 865 | ui.set_focus(Some(button)); | |
| 866 | ui.open_popup(menu); | |
| 867 | } | |
| 868 | let open = ui.popup_open(menu); | |
| 869 | if let Some(node) = ui.access(button) { | |
| 870 | node.set_has_popup(accesskit::HasPopup::Menu); | |
| 871 | node.set_expanded(open); | |
| 872 | } | |
| 873 | let items = [ | |
| 874 | ui::popup::Item { | |
| 875 | text: "From File System", | |
| 876 | icon: Some(art::NOTEBOOK), | |
| 877 | ..Default::default() | |
| 878 | }, | |
| 879 | ui::popup::Item { | |
| 880 | text: "From SMB Share", | |
| 881 | icon: Some(art::SERVER), | |
| 882 | disabled: cfg!(target_arch = "wasm32"), | |
| 883 | ..Default::default() | |
| 884 | }, | |
| 885 | ui::popup::Item { | |
| 886 | text: "Connect to Live Share", | |
| 887 | icon: Some(art::OPEN_SHARED), | |
| 888 | disabled: !cfg!(feature = "live"), | |
| 889 | ..Default::default() | |
| 890 | }, | |
| 891 | ]; | |
| 892 | match ui::popup::menu(ui, menu, ui::Anchor::Below(button), &items, None) { | |
| 893 | Some(0) => Some(crate::Command::OpenNotebook), | |
| 894 | Some(1) => Some(crate::Command::OpenFromServer(None)), | |
| 895 | #[cfg(feature = "live")] | |
| 896 | Some(2) => Some(crate::Command::OpenShared), | |
| 897 | _ => None, | |
| 898 | } | |
| 899 | } | |
| 900 | ||
| 842 | 901 | impl crate::State { |
| 843 | 902 | /// How wide the sidebar stands this frame as it eases open and shut. |
| 844 | 903 | pub(crate) fn sidebar_width(&mut self) -> f32 { |
| ... | ... | @@ -969,10 +1028,7 @@ impl crate::State { |
| 969 | 1028 | self.rename(target); |
| 970 | 1029 | } |
| 971 | 1030 | Some(Action::Renamed(keep)) => self.finish_renaming(keep), |
| 972 | Some(Action::NewNotebook) => self.commands.push(crate::Command::NewNotebook), | |
| 973 | Some(Action::OpenNotebook) => self.commands.push(crate::Command::OpenNotebook), | |
| 974 | #[cfg(feature = "live")] | |
| 975 | Some(Action::OpenShared) => self.open_shared(), | |
| 1031 | Some(Action::Command(command)) => self.commands.push(command), | |
| 976 | 1032 | Some(Action::SignIn(location)) => self |
| 977 | 1033 | .commands |
| 978 | 1034 | .push(crate::Command::OpenFromServer(Some(location))), |
| ... | ... | @@ -1454,22 +1510,9 @@ impl crate::State { |
| 1454 | 1510 | ( |
| 1455 | 1511 | "open", |
| 1456 | 1512 | Leading::Notebook(art::NOTEBOOK, None), |
| 1457 | "Open Notebook", | |
| 1513 | crate::commands::command(crate::commands::Id::OpenNotebook).title, | |
| 1458 | 1514 | crate::Command::OpenNotebook, |
| 1459 | 1515 | ), |
| 1460 | #[cfg(feature = "live")] | |
| 1461 | ( | |
| 1462 | "shared", | |
| 1463 | Leading::Icon(art::OPEN_SHARED), | |
| 1464 | "Open Shared Notebook", | |
| 1465 | crate::Command::OpenShared, | |
| 1466 | ), | |
| 1467 | ( | |
| 1468 | "server", | |
| 1469 | Leading::Icon(art::SERVER), | |
| 1470 | "Open Notebook from Server", | |
| 1471 | crate::Command::OpenFromServer(None), | |
| 1472 | ), | |
| 1473 | 1516 | ] |
| 1474 | 1517 | .into_iter() |
| 1475 | 1518 | .chain(crate::guide::OFFERED.then_some(( |
| ... | ... | @@ -1572,26 +1615,36 @@ impl crate::State { |
| 1572 | 1615 | ); |
| 1573 | 1616 | let mut chosen = None; |
| 1574 | 1617 | for (part, icon, label, command) in buttons { |
| 1575 | let (icon, tint) = icon.art(&theme); | |
| 1576 | let button = self.ui.leaf( | |
| 1577 | part, | |
| 1578 | Spec { | |
| 1579 | flags: Flags::CLICKABLE, | |
| 1580 | size: [fill(), px(BUTTON[1])], | |
| 1581 | icon: Some(icon), | |
| 1582 | tint: Some(tint), | |
| 1583 | text: Some(label), | |
| 1584 | fill: Some(theme.chip), | |
| 1585 | hover_fill: Some(theme.hover()), | |
| 1586 | hover_border: Some(theme.accent), | |
| 1587 | radius: 6.0, | |
| 1588 | center: true, | |
| 1589 | role: Some(accesskit::Role::Button), | |
| 1590 | ..Spec::default() | |
| 1591 | }, | |
| 1592 | ); | |
| 1593 | if button.clicked { | |
| 1594 | chosen = Some(command); | |
| 1618 | let spec = Spec { | |
| 1619 | flags: Flags::CLICKABLE, | |
| 1620 | size: [fill(), px(BUTTON[1])], | |
| 1621 | fill: Some(theme.chip), | |
| 1622 | hover_fill: Some(theme.hover()), | |
| 1623 | hover_border: Some(theme.accent), | |
| 1624 | radius: 6.0, | |
| 1625 | center: true, | |
| 1626 | role: Some(accesskit::Role::Button), | |
| 1627 | ..Spec::default() | |
| 1628 | }; | |
| 1629 | if matches!(command, crate::Command::OpenNotebook) { | |
| 1630 | chosen = open_notebook(&mut self.ui, spec).or(chosen); | |
| 1631 | } else { | |
| 1632 | let (icon, tint) = icon.art(&theme); | |
| 1633 | if self | |
| 1634 | .ui | |
| 1635 | .leaf( | |
| 1636 | part, | |
| 1637 | Spec { | |
| 1638 | icon: Some(icon), | |
| 1639 | tint: Some(tint), | |
| 1640 | text: Some(label), | |
| 1641 | ..spec | |
| 1642 | }, | |
| 1643 | ) | |
| 1644 | .clicked | |
| 1645 | { | |
| 1646 | chosen = Some(command); | |
| 1647 | } | |
| 1595 | 1648 | } |
| 1596 | 1649 | } |
| 1597 | 1650 | self.commands.extend(chosen); |
crates/snowbound/tests/replay.rs+134-29| ... | ... | @@ -796,37 +796,142 @@ fn stop_sharing_ends_the_share() { |
| 796 | 796 | assert!(!kept.contains("code"), "still kept: {kept}"); |
| 797 | 797 | } |
| 798 | 798 | |
| 799 | /// Open Shared Notebook is where people look with notebooks open: in the sidebar's footer, | |
| 800 | /// and in the palette, which opens it. | |
| 801 | #[cfg(feature = "live")] | |
| 802 | 799 | #[test] |
| 803 | fn open_shared_notebook_is_offered_with_notebooks_open() { | |
| 804 | let scratch = Scratch::new("open-shared"); | |
| 805 | std::fs::write( | |
| 806 | scratch.0.join("settings.json"), | |
| 807 | r#"{"user_name": "Snowbound Test", "sidebar": true}"#, | |
| 808 | ) | |
| 809 | .unwrap(); | |
| 810 | let notebook = | |
| 811 | Path::new(env!("CARGO_MANIFEST_DIR")).join("../../corpus/cross-container/candidate"); | |
| 812 | let mut steps = vec!["accessibility sidebar", "modifiers command shift", "key p"]; | |
| 813 | steps.extend([ | |
| 814 | "modifiers", | |
| 815 | "settle", | |
| 816 | "type Open Shared", | |
| 817 | "settle", | |
| 818 | "key Enter", | |
| 819 | ]); | |
| 820 | steps.extend(["settle", "accessibility dialog"]); | |
| 821 | let variables = [("SNOWBOUND_LIVE", "off"), ("SNOWBOUND_LIVE_RELAY", "off")]; | |
| 822 | let [sidebar, dialog] = replay_with(&scratch, Some(&notebook), &steps, &variables) | |
| 823 | .try_into() | |
| 800 | fn open_notebook_routes_from_the_sidebar_and_welcome() { | |
| 801 | for welcome in [false, true] { | |
| 802 | let scratch = Scratch::new(if welcome { | |
| 803 | "welcome-open" | |
| 804 | } else { | |
| 805 | "sidebar-open" | |
| 806 | }); | |
| 807 | std::fs::write( | |
| 808 | scratch.0.join("settings.json"), | |
| 809 | r#"{"user_name": "Snowbound Test", "sidebar": true}"#, | |
| 810 | ) | |
| 824 | 811 | .unwrap(); |
| 825 | assert!(sidebar.contains(r#""Open Shared""#), "{sidebar}"); | |
| 826 | assert!( | |
| 827 | dialog.contains(r#"Dialog "Open Shared Notebook""#), | |
| 828 | "{dialog}" | |
| 829 | ); | |
| 812 | let notebook = | |
| 813 | Path::new(env!("CARGO_MANIFEST_DIR")).join("../../corpus/cross-container/candidate"); | |
| 814 | let mut steps = Vec::new(); | |
| 815 | if welcome { | |
| 816 | steps.extend([ | |
| 817 | "modifiers command shift", | |
| 818 | "key p", | |
| 819 | "modifiers", | |
| 820 | "settle", | |
| 821 | "type Close This Notebook", | |
| 822 | "key Enter", | |
| 823 | "settle", | |
| 824 | ]); | |
| 825 | } | |
| 826 | steps.extend([ | |
| 827 | "accessibility initial", | |
| 828 | "access focus Open Notebook", | |
| 829 | "key Enter", | |
| 830 | "accessibility menu", | |
| 831 | "key Escape", | |
| 832 | "accessibility escaped", | |
| 833 | "key Space", | |
| 834 | "accessibility space-menu", | |
| 835 | "move 1170 10", | |
| 836 | "press", | |
| 837 | "release", | |
| 838 | "accessibility outside", | |
| 839 | "key Enter", | |
| 840 | "key Down", | |
| 841 | "key Down", | |
| 842 | "key Enter", | |
| 843 | "accessibility keyboard-smb", | |
| 844 | "key Escape", | |
| 845 | "access click Open Notebook", | |
| 846 | "access click From SMB Share", | |
| 847 | "accessibility access-smb", | |
| 848 | "key Escape", | |
| 849 | "accessibility cancelled", | |
| 850 | ]); | |
| 851 | #[cfg(feature = "live")] | |
| 852 | steps.extend([ | |
| 853 | "access click Open Notebook", | |
| 854 | "access click Connect to Live Share", | |
| 855 | "accessibility live", | |
| 856 | "key Escape", | |
| 857 | "accessibility live-cancelled", | |
| 858 | "modifiers command shift", | |
| 859 | "key p", | |
| 860 | "modifiers", | |
| 861 | "settle", | |
| 862 | "type Open Shared", | |
| 863 | "key Enter", | |
| 864 | "accessibility palette-live", | |
| 865 | ]); | |
| 866 | let variables = [("SNOWBOUND_LIVE", "off"), ("SNOWBOUND_LIVE_RELAY", "off")]; | |
| 867 | let trees = replay_with(&scratch, Some(&notebook), &steps, &variables); | |
| 868 | assert_eq!( | |
| 869 | trees[0].matches(r#"Button "Open Notebook""#).count(), | |
| 870 | 1, | |
| 871 | "{}", | |
| 872 | trees[0] | |
| 873 | ); | |
| 874 | assert_eq!( | |
| 875 | trees[0].contains("No notebooks open"), | |
| 876 | welcome, | |
| 877 | "{}", | |
| 878 | trees[0] | |
| 879 | ); | |
| 880 | for tree in [&trees[1], &trees[3]] { | |
| 881 | assert_eq!( | |
| 882 | menu_items(tree), | |
| 883 | [ | |
| 884 | "From File System", | |
| 885 | "From SMB Share", | |
| 886 | "Connect to Live Share" | |
| 887 | ] | |
| 888 | ); | |
| 889 | assert!( | |
| 890 | tree.contains(r#"Button "Open Notebook" [expanded]"#), | |
| 891 | "{tree}" | |
| 892 | ); | |
| 893 | } | |
| 894 | let live = trees[1] | |
| 895 | .lines() | |
| 896 | .find(|line| line.contains(r#"MenuItem "Connect to Live Share""#)) | |
| 897 | .unwrap(); | |
| 898 | assert_eq!( | |
| 899 | live.contains("[disabled]"), | |
| 900 | !cfg!(feature = "live"), | |
| 901 | "{live}" | |
| 902 | ); | |
| 903 | for tree in [&trees[2], &trees[4], &trees[7]] { | |
| 904 | assert!( | |
| 905 | tree.contains(r#"Button "Open Notebook" [focused]"#), | |
| 906 | "{tree}" | |
| 907 | ); | |
| 908 | assert!(!tree.contains("MenuItem"), "{tree}"); | |
| 909 | } | |
| 910 | for tree in &trees[5..7] { | |
| 911 | assert_eq!( | |
| 912 | tree.matches(r#"Dialog "Open Notebook from Server""#) | |
| 913 | .count(), | |
| 914 | 1, | |
| 915 | "{tree}" | |
| 916 | ); | |
| 917 | assert!(!tree.contains("MenuItem"), "{tree}"); | |
| 918 | } | |
| 919 | #[cfg(feature = "live")] | |
| 920 | { | |
| 921 | for tree in [&trees[8], &trees[10]] { | |
| 922 | assert_eq!( | |
| 923 | tree.matches(r#"Dialog "Open Shared Notebook""#).count(), | |
| 924 | 1, | |
| 925 | "{tree}" | |
| 926 | ); | |
| 927 | } | |
| 928 | assert!( | |
| 929 | trees[9].contains(r#"Button "Open Notebook" [focused]"#), | |
| 930 | "{}", | |
| 931 | trees[9] | |
| 932 | ); | |
| 933 | } | |
| 934 | } | |
| 830 | 935 | } |
| 831 | 936 | |
| 832 | 937 | /// Switching between Metal and OpenGL, again and again, leaves the page, its edits and the |