Another project
0

Configure Feed

Select the types of activity you want to include in your feed.

test(ui): widget keyboard activation coverage

Lewis: May this revision serve well! <lu5a@proton.me>

author
Lewis
date (May 6, 2026, 10:35 AM +0300) commit b30fcf85 parent acee269e change-id vznzvmxt
+402
+101
crates/bone-ui/src/widgets/dialog.rs
··· 578 578 } 579 579 580 580 #[test] 581 + fn enter_on_focused_button_emits_activation() { 582 + let confirm_id = modal_id().child(WidgetKey::new("confirm")); 583 + let cancel_id = modal_id().child(WidgetKey::new("cancel")); 584 + let buttons = [ 585 + DialogButton::primary(confirm_id, StringKey::new("dialog.ok")), 586 + DialogButton::secondary(cancel_id, StringKey::new("dialog.cancel")), 587 + ]; 588 + let mut focus = FocusManager::new(); 589 + let prev = HitState::new(); 590 + let mut warm = InputSnapshot::idle(FrameInstant::ZERO); 591 + focus.request_focus(confirm_id); 592 + let _ = run_dialog(&mut focus, &mut warm, &prev, &buttons); 593 + assert_eq!(focus.focused(), Some(confirm_id)); 594 + 595 + let mut snap = InputSnapshot::idle(FrameInstant::ZERO); 596 + snap.keys_pressed.push(KeyEvent::new( 597 + KeyCode::Named(NamedKey::Enter), 598 + ModifierMask::NONE, 599 + )); 600 + let (response, _) = run_dialog(&mut focus, &mut snap, &prev, &buttons); 601 + assert_eq!(response.activated, Some(confirm_id)); 602 + } 603 + 604 + fn run_confirmation( 605 + focus: &mut FocusManager, 606 + snap: &mut InputSnapshot, 607 + prev: &HitState, 608 + destructive: bool, 609 + ) -> (super::ConfirmationResponse, HitState) { 610 + let theme = Arc::new(Theme::light()); 611 + let table = HotkeyTable::new(); 612 + let mut hits = HitFrame::new(); 613 + let response = { 614 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 615 + let mut ctx = FrameCtx::new( 616 + theme, 617 + snap, 618 + focus, 619 + &table, 620 + StringTable::empty(), 621 + &mut hits, 622 + prev, 623 + &mut a11y, 624 + ); 625 + show_confirmation( 626 + &mut ctx, 627 + ConfirmationDialog { 628 + id: modal_id(), 629 + viewport: viewport(), 630 + size: LayoutSize::new(LayoutPx::new(400.0), LayoutPx::new(240.0)), 631 + title: StringKey::new("dialog.title"), 632 + message: StringKey::new("dialog.message"), 633 + confirm_label: StringKey::new("dialog.ok"), 634 + cancel_label: StringKey::new("dialog.cancel"), 635 + destructive, 636 + }, 637 + ) 638 + }; 639 + let next = resolve(prev, &hits, snap, focus.focused()); 640 + (response, next) 641 + } 642 + 643 + #[test] 644 + fn enter_on_seeded_cancel_button_emits_cancel_outcome() { 645 + let cancel_id = modal_id().child(WidgetKey::new("cancel")); 646 + let mut focus = FocusManager::new(); 647 + let prev = HitState::new(); 648 + let mut warm = InputSnapshot::idle(FrameInstant::ZERO); 649 + let _ = run_confirmation(&mut focus, &mut warm, &prev, false); 650 + assert_eq!( 651 + focus.focused(), 652 + Some(cancel_id), 653 + "confirmation seeds focus on cancel button (buttons[0])", 654 + ); 655 + 656 + let mut enter = InputSnapshot::idle(FrameInstant::ZERO); 657 + enter.keys_pressed.push(KeyEvent::new( 658 + KeyCode::Named(NamedKey::Enter), 659 + ModifierMask::NONE, 660 + )); 661 + let (response, _) = run_confirmation(&mut focus, &mut enter, &prev, false); 662 + assert_eq!(response.outcome, Some(ConfirmationOutcome::Cancel)); 663 + } 664 + 665 + #[test] 666 + fn escape_on_open_confirmation_emits_cancel_outcome() { 667 + let mut focus = FocusManager::new(); 668 + let prev = HitState::new(); 669 + let mut warm = InputSnapshot::idle(FrameInstant::ZERO); 670 + let _ = run_confirmation(&mut focus, &mut warm, &prev, false); 671 + 672 + let mut esc = InputSnapshot::idle(FrameInstant::ZERO); 673 + esc.keys_pressed.push(KeyEvent::new( 674 + KeyCode::Named(NamedKey::Escape), 675 + ModifierMask::NONE, 676 + )); 677 + let (response, _) = run_confirmation(&mut focus, &mut esc, &prev, false); 678 + assert_eq!(response.outcome, Some(ConfirmationOutcome::Cancel)); 679 + } 680 + 681 + #[test] 581 682 fn click_confirm_button_emits_confirm_outcome() { 582 683 let mut focus = FocusManager::new(); 583 684 let mut prev = HitState::new();
+55
crates/bone-ui/src/widgets/menu.rs
··· 1112 1112 }); 1113 1113 assert_eq!(state.open, Some(entries[1].id)); 1114 1114 } 1115 + 1116 + #[test] 1117 + fn arrow_down_on_focused_entry_opens_its_menu() { 1118 + use crate::input::{KeyCode, KeyEvent, ModifierMask, NamedKey}; 1119 + 1120 + let entries = vec![entry("file"), entry("edit")]; 1121 + let mut state = bar_state(); 1122 + let theme = Arc::new(Theme::light()); 1123 + let table = HotkeyTable::new(); 1124 + let mut focus = FocusManager::new(); 1125 + let bar_rect = LayoutRect::new( 1126 + LayoutPos::new(LayoutPx::ZERO, LayoutPx::ZERO), 1127 + LayoutSize::new(LayoutPx::new(400.0), LayoutPx::new(24.0)), 1128 + ); 1129 + focus.request_focus(entries[0].id); 1130 + let prev = HitState::new(); 1131 + 1132 + [ 1133 + InputSnapshot::idle(FrameInstant::ZERO), 1134 + { 1135 + let mut s = InputSnapshot::idle(FrameInstant::ZERO); 1136 + s.keys_pressed.push(KeyEvent::new( 1137 + KeyCode::Named(NamedKey::ArrowDown), 1138 + ModifierMask::NONE, 1139 + )); 1140 + s 1141 + }, 1142 + ] 1143 + .into_iter() 1144 + .for_each(|mut snap| { 1145 + let mut hits = HitFrame::new(); 1146 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 1147 + let mut ctx = FrameCtx::new( 1148 + theme.clone(), 1149 + &mut snap, 1150 + &mut focus, 1151 + &table, 1152 + StringTable::empty(), 1153 + &mut hits, 1154 + &prev, 1155 + &mut a11y, 1156 + ); 1157 + let _ = show_menu_bar( 1158 + &mut ctx, 1159 + MenuBar::new( 1160 + menu_bar_id(), 1161 + bar_rect, 1162 + StringKey::new("test.menu_bar"), 1163 + &entries, 1164 + &mut state, 1165 + ), 1166 + ); 1167 + }); 1168 + assert_eq!(state.open, Some(entries[0].id)); 1169 + } 1115 1170 }
+28
crates/bone-ui/src/widgets/panel.rs
··· 371 371 } 372 372 373 373 #[test] 374 + fn enter_on_focused_titlebar_toggles_collapsed() { 375 + use crate::input::{KeyCode, KeyEvent, ModifierMask, NamedKey}; 376 + 377 + let toggle_id = panel_id().child(WidgetKey::new("titlebar")); 378 + let mut state = PanelState::open(); 379 + let mut focus = FocusManager::new(); 380 + let prev = HitState::new(); 381 + focus.request_focus(toggle_id); 382 + let mut warm = InputSnapshot::idle(FrameInstant::ZERO); 383 + let _ = render(&mut state, &mut focus, &mut warm, &prev, Some(titlebar(true))); 384 + assert_eq!(focus.focused(), Some(toggle_id)); 385 + 386 + let mut enter = InputSnapshot::idle(FrameInstant::ZERO); 387 + enter 388 + .keys_pressed 389 + .push(KeyEvent::new(KeyCode::Named(NamedKey::Enter), ModifierMask::NONE)); 390 + let _ = render(&mut state, &mut focus, &mut enter, &prev, Some(titlebar(true))); 391 + assert!(state.collapsed, "Enter on focused titlebar must collapse"); 392 + 393 + let mut space = InputSnapshot::idle(FrameInstant::ZERO); 394 + space 395 + .keys_pressed 396 + .push(KeyEvent::new(KeyCode::Named(NamedKey::Space), ModifierMask::NONE)); 397 + let _ = render(&mut state, &mut focus, &mut space, &prev, Some(titlebar(true))); 398 + assert!(!state.collapsed, "Space on focused titlebar must expand"); 399 + } 400 + 401 + #[test] 374 402 fn no_titlebar_returns_body_equal_to_panel() { 375 403 let mut state = PanelState::open(); 376 404 let mut focus = FocusManager::new();
+63
crates/bone-ui/src/widgets/property_grid.rs
··· 606 606 } 607 607 608 608 #[test] 609 + fn tab_advances_focus_through_rows_in_order() { 610 + use crate::focus::FocusRequest; 611 + 612 + let mut bool_a = BoolEditor::new(false); 613 + let mut bool_b = BoolEditor::new(false); 614 + let mut clipboard = MemoryClipboard::default(); 615 + let mut focus = FocusManager::new(); 616 + let prev = HitState::new(); 617 + let theme = Arc::new(Theme::light()); 618 + let table = HotkeyTable::new(); 619 + let alpha_id = grid_id().child(WidgetKey::new("row_a")); 620 + let beta_id = grid_id().child(WidgetKey::new("row_b")); 621 + let mut render = |focus: &mut FocusManager, 622 + bool_a: &mut BoolEditor, 623 + bool_b: &mut BoolEditor, 624 + mut snap: InputSnapshot| { 625 + let mut hits = HitFrame::new(); 626 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 627 + let mut ctx = FrameCtx::new( 628 + theme.clone(), 629 + &mut snap, 630 + focus, 631 + &table, 632 + StringTable::empty(), 633 + &mut hits, 634 + &prev, 635 + &mut a11y, 636 + ); 637 + let mut rows = [ 638 + PropertyRow { 639 + id: alpha_id, 640 + label: StringKey::new("prop.a"), 641 + editor: bool_a, 642 + read_only: false, 643 + }, 644 + PropertyRow { 645 + id: beta_id, 646 + label: StringKey::new("prop.b"), 647 + editor: bool_b, 648 + read_only: false, 649 + }, 650 + ]; 651 + let _ = show_property_grid( 652 + &mut ctx, 653 + PropertyGrid::new(grid_id(), rect(), StringKey::new("test.grid"), &mut rows), 654 + &mut clipboard, 655 + ); 656 + }; 657 + 658 + focus.request(FocusRequest::Advance); 659 + render(&mut focus, &mut bool_a, &mut bool_b, InputSnapshot::idle(FrameInstant::ZERO)); 660 + assert_eq!(focus.focused(), Some(alpha_id)); 661 + 662 + focus.request(FocusRequest::Advance); 663 + render(&mut focus, &mut bool_a, &mut bool_b, InputSnapshot::idle(FrameInstant::ZERO)); 664 + assert_eq!(focus.focused(), Some(beta_id)); 665 + 666 + focus.request(FocusRequest::Retreat); 667 + render(&mut focus, &mut bool_a, &mut bool_b, InputSnapshot::idle(FrameInstant::ZERO)); 668 + assert_eq!(focus.focused(), Some(alpha_id)); 669 + } 670 + 671 + #[test] 609 672 fn rows_paint_label_per_row() { 610 673 let mut bool_editor = BoolEditor::new(false); 611 674 let mut select_editor = SelectionEditor::new(
+35
crates/bone-ui/src/widgets/ribbon.rs
··· 503 503 assert!(response.activated_tab.is_none()); 504 504 } 505 505 506 + #[test] 507 + fn arrow_then_enter_switches_active_tab_via_keyboard() { 508 + use crate::input::{KeyCode, KeyEvent, ModifierMask, NamedKey}; 509 + 510 + let tabs = vec![make_ribbon_tab("home"), make_ribbon_tab("sketch")]; 511 + let mut state = RibbonState::default(); 512 + let mut focus = FocusManager::new(); 513 + let prev = HitState::new(); 514 + focus.request_focus(tabs[0].tab.id); 515 + let mut warm = InputSnapshot::idle(FrameInstant::ZERO); 516 + let _ = render(&tabs, tabs[0].tab.id, &mut state, &mut focus, &mut warm, &prev); 517 + assert_eq!(focus.focused(), Some(tabs[0].tab.id)); 518 + 519 + let mut arrow = InputSnapshot::idle(FrameInstant::ZERO); 520 + arrow 521 + .keys_pressed 522 + .push(KeyEvent::new(KeyCode::Named(NamedKey::ArrowRight), ModifierMask::NONE)); 523 + let _ = render(&tabs, tabs[0].tab.id, &mut state, &mut focus, &mut arrow, &prev); 524 + assert_eq!(focus.focused(), Some(tabs[1].tab.id)); 525 + 526 + let mut enter = InputSnapshot::idle(FrameInstant::ZERO); 527 + enter 528 + .keys_pressed 529 + .push(KeyEvent::new(KeyCode::Named(NamedKey::Enter), ModifierMask::NONE)); 530 + let (response, _) = render( 531 + &tabs, 532 + tabs[0].tab.id, 533 + &mut state, 534 + &mut focus, 535 + &mut enter, 536 + &prev, 537 + ); 538 + assert_eq!(response.activated_tab, Some(tabs[1].tab.id)); 539 + } 540 + 506 541 fn press(pos: LayoutPos) -> InputSnapshot { 507 542 let mut s = InputSnapshot::idle(FrameInstant::ZERO); 508 543 s.pointer = Some(PointerSample::new(pos));
+21
crates/bone-ui/src/widgets/status_bar.rs
··· 388 388 } 389 389 390 390 #[test] 391 + fn enter_on_focused_interactive_item_activates_it() { 392 + use crate::input::{KeyCode, KeyEvent, ModifierMask, NamedKey}; 393 + 394 + let items = items(); 395 + let interactive_id = items[1].id; 396 + let mut focus = FocusManager::new(); 397 + let prev = HitState::new(); 398 + focus.request_focus(interactive_id); 399 + let mut warm = InputSnapshot::idle(FrameInstant::ZERO); 400 + let _ = render(&items, &mut focus, &mut warm, &prev); 401 + assert_eq!(focus.focused(), Some(interactive_id)); 402 + 403 + let mut enter = InputSnapshot::idle(FrameInstant::ZERO); 404 + enter 405 + .keys_pressed 406 + .push(KeyEvent::new(KeyCode::Named(NamedKey::Enter), ModifierMask::NONE)); 407 + let (response, _) = render(&items, &mut focus, &mut enter, &prev); 408 + assert_eq!(response.activated, Some(interactive_id)); 409 + } 410 + 411 + #[test] 391 412 fn overflow_packs_center_and_end_after_start_without_underflow() { 392 413 let crowded = vec![ 393 414 StatusItem::new(
+22
crates/bone-ui/src/widgets/toast.rs
··· 372 372 } 373 373 374 374 #[test] 375 + fn enter_on_focused_close_dismisses_toast() { 376 + use crate::input::{KeyCode, KeyEvent, ModifierMask, NamedKey}; 377 + 378 + let close_id = toast_id().child(WidgetKey::new("close")); 379 + let mut state = ToastState::fresh(); 380 + let mut focus = FocusManager::new(); 381 + let prev = HitState::new(); 382 + focus.request_focus(close_id); 383 + let mut warm = InputSnapshot::idle(FrameInstant::ZERO); 384 + let _ = render(&mut state, &mut focus, &mut warm, &prev, 4000); 385 + assert_eq!(focus.focused(), Some(close_id)); 386 + 387 + let mut enter = InputSnapshot::idle(FrameInstant::ZERO); 388 + enter.keys_pressed.push(KeyEvent::new( 389 + KeyCode::Named(NamedKey::Enter), 390 + ModifierMask::NONE, 391 + )); 392 + let _ = render(&mut state, &mut focus, &mut enter, &prev, 4000); 393 + assert!(state.dismissed, "Enter on focused close button must dismiss"); 394 + } 395 + 396 + #[test] 375 397 fn click_close_dismisses_toast() { 376 398 let mut state = ToastState::fresh(); 377 399 let mut focus = FocusManager::new();
+77
crates/bone-ui/src/widgets/toolbar.rs
··· 560 560 s.pointer = Some(PointerSample::new(pos)); 561 561 s 562 562 } 563 + 564 + #[test] 565 + fn keyboard_space_on_focused_item_activates_it() { 566 + use crate::input::{KeyCode, KeyEvent, ModifierMask, NamedKey}; 567 + 568 + let items = items(3); 569 + let rect = LayoutRect::new( 570 + LayoutPos::new(LayoutPx::ZERO, LayoutPx::ZERO), 571 + LayoutSize::new(LayoutPx::new(200.0), LayoutPx::new(28.0)), 572 + ); 573 + let mut overflow_open = false; 574 + let mut focus = FocusManager::new(); 575 + let prev = HitState::new(); 576 + let mut snap_focus = InputSnapshot::idle(FrameInstant::ZERO); 577 + focus.request_focus(items[1].id); 578 + let _ = render( 579 + &items, 580 + rect, 581 + &mut overflow_open, 582 + &mut focus, 583 + &mut snap_focus, 584 + &prev, 585 + ); 586 + assert_eq!(focus.focused(), Some(items[1].id)); 587 + 588 + let mut snap_space = InputSnapshot::idle(FrameInstant::ZERO); 589 + snap_space 590 + .keys_pressed 591 + .push(KeyEvent::new(KeyCode::Named(NamedKey::Space), ModifierMask::NONE)); 592 + let (response, _) = render( 593 + &items, 594 + rect, 595 + &mut overflow_open, 596 + &mut focus, 597 + &mut snap_space, 598 + &prev, 599 + ); 600 + assert_eq!(response.activated, Some(items[1].id)); 601 + } 602 + 603 + #[test] 604 + fn keyboard_enter_on_focused_item_activates_it() { 605 + use crate::input::{KeyCode, KeyEvent, ModifierMask, NamedKey}; 606 + 607 + let items = items(2); 608 + let rect = LayoutRect::new( 609 + LayoutPos::new(LayoutPx::ZERO, LayoutPx::ZERO), 610 + LayoutSize::new(LayoutPx::new(160.0), LayoutPx::new(28.0)), 611 + ); 612 + let mut overflow_open = false; 613 + let mut focus = FocusManager::new(); 614 + let prev = HitState::new(); 615 + let mut warm = InputSnapshot::idle(FrameInstant::ZERO); 616 + focus.request_focus(items[0].id); 617 + let _ = render( 618 + &items, 619 + rect, 620 + &mut overflow_open, 621 + &mut focus, 622 + &mut warm, 623 + &prev, 624 + ); 625 + 626 + let mut enter = InputSnapshot::idle(FrameInstant::ZERO); 627 + enter 628 + .keys_pressed 629 + .push(KeyEvent::new(KeyCode::Named(NamedKey::Enter), ModifierMask::NONE)); 630 + let (response, _) = render( 631 + &items, 632 + rect, 633 + &mut overflow_open, 634 + &mut focus, 635 + &mut enter, 636 + &prev, 637 + ); 638 + assert_eq!(response.activated, Some(items[0].id)); 639 + } 563 640 }