From 8794a42d420c6ee0cbcd602df56432c7b6bd4bc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Mon, 28 Sep 2026 04:09:49 -0400 Subject: [PATCH] =?UTF-8?q?Misc=20=C2=B7=20Terms=20and=20notices:=20tests?= =?UTF-8?q?=20for=20every=20way=20out;=20switch=20labels=20wrap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tests now press Decline and the scrim, see focus return to the opener after Accept, see necessary cookies stay on and off the Tab path, and see the license list mark its row and read each version and license. lists::ListItem marks its description and selected state for tests. forms::Switch's label wraps in its own block, as Checkbox's does. Ely's own row in the gallery shows both of its license texts. --- examples/gallery/pages/misc/mod.rs | 1 + examples/gallery/pages/misc/notices.rs | 6 +- src/forms/switch.rs | 8 ++- src/forms/tests/other.rs | 7 +- src/lists/item.rs | 8 ++- src/misc/cookies.rs | 22 +++--- src/misc/tests/notices.rs | 95 +++++++++++++++++++++++--- 7 files changed, 123 insertions(+), 24 deletions(-) diff --git a/examples/gallery/pages/misc/mod.rs b/examples/gallery/pages/misc/mod.rs index 8eb6b4b..d68a1b5 100644 --- a/examples/gallery/pages/misc/mod.rs +++ b/examples/gallery/pages/misc/mod.rs @@ -63,6 +63,7 @@ pub const PAGE: Page = Page { Step::Key("escape"), Step::DownAt("cookies", 230.0, 84.0), Step::UpAt("cookies", 230.0, 84.0), + Step::HoverAt("cookies", 20.0, 20.0), Step::Shot("cookies-choosing"), ], }; diff --git a/examples/gallery/pages/misc/notices.rs b/examples/gallery/pages/misc/notices.rs index 07ca54b..1d0ba34 100644 --- a/examples/gallery/pages/misc/notices.rs +++ b/examples/gallery/pages/misc/notices.rs @@ -17,7 +17,11 @@ fn packages() -> [Package; 4] { "Ely GPUI Component", "0.1.0", "MIT OR Apache-2.0", - include_str!("../../../../LICENSE-MIT"), + concat!( + include_str!("../../../../LICENSE-MIT"), + "\n\n", + include_str!("../../../../LICENSE-APACHE") + ), ), Package::new( "Lucide", diff --git a/src/forms/switch.rs b/src/forms/switch.rs index db5700c..5da6e96 100644 --- a/src/forms/switch.rs +++ b/src/forms/switch.rs @@ -140,6 +140,12 @@ impl RenderOnce for Switch { }) }) .child(rail) - .children(self.label) + .children(self.label.map(|label| { + div() + .debug_selector(|| "switch-label".into()) + .flex_1() + .min_w_0() + .child(label) + })) } } diff --git a/src/forms/tests/other.rs b/src/forms/tests/other.rs index 87d970c..aaf784e 100644 --- a/src/forms/tests/other.rs +++ b/src/forms/tests/other.rs @@ -5,7 +5,7 @@ use gpui::{ use super::setup; use crate::{ - forms::{Checkbox, Choice, IconPicker, InlineEdit, RadioGroup, SignaturePad, Stroke}, + forms::{Checkbox, Choice, IconPicker, InlineEdit, RadioGroup, SignaturePad, Stroke, Switch}, primitives::{FocusNext, IconName}, }; @@ -145,7 +145,7 @@ fn an_inline_edit_takes_tab_opens_on_enter_and_takes_focus_back(cx: &mut TestApp ); } -/// A checkbox and a radio whose labels run past their narrow box. +/// A checkbox, a radio and a switch whose labels run past their narrow box. struct Worded; impl Render for Worded { @@ -158,6 +158,7 @@ impl Render for Worded { "worded-radios", [Choice::new("one", label)], )) + .child(Switch::new("worded-switch", false).label(label)) } } @@ -166,7 +167,7 @@ fn long_labels_wrap_inside_their_box(cx: &mut TestAppContext) { setup(cx); let (_, cx) = cx.add_window_view(|_, _| Worded); cx.run_until_parked(); - for selector in ["checkbox-label", "radio-label"] { + for selector in ["checkbox-label", "radio-label", "switch-label"] { let label = cx.debug_bounds(selector).expect("the label draws"); assert!(label.right() <= px(200.0), "{selector}: {label:?}"); } diff --git a/src/lists/item.rs b/src/lists/item.rs index addd984..7e50f49 100644 --- a/src/lists/item.rs +++ b/src/lists/item.rs @@ -153,7 +153,10 @@ impl RenderOnce for ListItem { } else { gpui::transparent_black() }) - .when(self.selected, |row| row.bg(colors.active)) + .when(self.selected, |row| { + row.debug_selector(|| format!("item-selected-{}", self.title)) + .bg(colors.active) + }) .when(self.disabled, |row| row.opacity(0.5)) .when_some(self.on_click.filter(|_| pressable), |row, on_click| { row.cursor_pointer() @@ -182,7 +185,8 @@ impl RenderOnce for ListItem { }), ) .children(self.description.map(|text| { - line(text) + line(text.clone()) + .debug_selector(|| format!("item-description-{text}")) .text_size(theme.text_size(TextSize::Sm)) .text_color(colors.fg_muted) })) diff --git a/src/misc/cookies.rs b/src/misc/cookies.rs index 8639852..e6ac249 100644 --- a/src/misc/cookies.rs +++ b/src/misc/cookies.rs @@ -1,7 +1,8 @@ use std::rc::Rc; use gpui::{ - App, ElementId, IntoElement, ParentElement, RenderOnce, SharedString, Styled, Window, div, + App, ElementId, InteractiveElement, IntoElement, ParentElement, RenderOnce, SharedString, + Styled, Window, div, }; use crate::{ @@ -104,13 +105,18 @@ impl RenderOnce for CookieBanner { .flex() .flex_col() .gap_3() - .child(row( - Switch::new((id.clone(), "necessary"), true) - .label("Necessary") - .disabled(true), - "Keeps the site working; always on.".into(), - cx, - )) + .child({ + let (on, rests) = (true, true); + div() + .debug_selector(|| format!("cookie-necessary-{on}-{rests}")) + .child(row( + Switch::new((id.clone(), "necessary"), on) + .label("Necessary") + .disabled(rests), + "Keeps the site working; always on.".into(), + cx, + )) + }) .children(rows) }); let (all, none, saved) = (answer.clone(), answer.clone(), answer); diff --git a/src/misc/tests/notices.rs b/src/misc/tests/notices.rs index d2c6575..a41750c 100644 --- a/src/misc/tests/notices.rs +++ b/src/misc/tests/notices.rs @@ -1,9 +1,13 @@ use std::cell::{Cell, RefCell}; -use gpui::{AnyElement, IntoElement, TestAppContext}; +use gpui::{ + AnyElement, App, Context, FocusHandle, IntoElement, Modifiers, ParentElement, Render, Styled, + TestAppContext, Window, div, point, px, +}; -use super::{at_root, press, shown, stage}; +use super::{at_root, press, settle, shown, stage}; use crate::misc::{ConsentDialog, CookieBanner, LicenseViewer, Package}; +use crate::{buttons::Button, primitives::FocusScope, theme::Theme}; thread_local! { static SAID: RefCell> = const { RefCell::new(Vec::new()) }; @@ -24,20 +28,25 @@ fn licenses() -> AnyElement { [ Package::new("Alpha", "1.0", "MIT", "Permission is hereby granted"), Package::new("Beta", "2.3", "ISC", "Permission to use, copy, modify"), + Package::new("Gamma", "0.9", "OFL-1.1", "Permission is hereby granted"), ], ) .into_any_element() } -/// The first package's license shows; a move down the list shows the next one's. +/// Each row reads its version and license; the first license shows, and moves down the list show the next ones, the list marking each. #[gpui::test] fn a_license_viewer_shows_the_chosen_license(cx: &mut TestAppContext) { let (_, cx) = stage(licenses, cx); - assert!(shown("license-Alpha", cx)); + assert!(shown("license-Alpha", cx) && shown("item-description-2.3 ยท ISC", cx)); press("tab", cx); press("down", cx); - press("space", cx); - assert!(shown("license-Beta", cx)); + assert!(shown("license-Beta", cx) && shown("item-selected-Beta", cx)); + press("down", cx); + assert!( + shown("license-Gamma", cx), + "the list kept its mark on Beta, so its cursor went on from there" + ); } fn consent() -> AnyElement { @@ -78,13 +87,77 @@ fn consent_waits_for_the_box(cx: &mut TestAppContext) { assert_eq!(said(), ["accepted"], "Accept answers alone"); } -/// Escape declines, once. +/// Escape, Decline and a press on the scrim each decline, once. #[gpui::test] -fn escape_declines_consent(cx: &mut TestAppContext) { +fn every_other_way_out_declines(cx: &mut TestAppContext) { let (_, cx) = stage(consent, cx); press("tab", cx); press("escape", cx); - assert_eq!(said(), ["declined"]); + OPEN.set(true); + cx.update(|window, _| window.refresh()); + settle(cx); + press("tab", cx); + press("tab", cx); + press("enter", cx); + OPEN.set(true); + cx.update(|window, _| window.refresh()); + settle(cx); + cx.simulate_click(point(px(3.0), px(3.0)), Modifiers::none()); + settle(cx); + assert_eq!(said(), ["declined", "declined", "declined"]); +} + +/// A page with a button that opens the consent dialog, which answers into `said`. +struct Opener { + root: FocusHandle, + button: FocusHandle, +} + +impl Render for Opener { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + let view = cx.entity(); + FocusScope::new(&self.root) + .root() + .size_full() + .child( + Button::new("open", "Terms") + .focus_handle(&self.button) + .on_click(move |_, _, cx| { + OPEN.set(true); + view.update(cx, |_, cx| cx.notify()); + }), + ) + .child(div().child(consent())) + } +} + +/// Accept hands focus back to the button that opened the terms. +#[gpui::test] +fn consent_hands_focus_back_to_its_opener(cx: &mut TestAppContext) { + OPEN.set(false); + cx.update(|cx: &mut App| { + Theme::init(cx); + crate::forms::bind_keys(cx); + cx.bind_keys([ + gpui::KeyBinding::new("tab", crate::primitives::FocusNext, None), + gpui::KeyBinding::new("shift-tab", crate::primitives::FocusPrev, None), + ]); + }); + let (view, cx) = cx.add_window_view(|_, cx| Opener { + root: cx.focus_handle(), + button: cx.focus_handle(), + }); + let button = view.read_with(cx, |opener, _| opener.button.clone()); + cx.update(|window, _| window.focus(&button)); + settle(cx); + press("enter", cx); + press("tab", cx); + press("space", cx); + press("tab", cx); + press("tab", cx); + press("enter", cx); + assert_eq!(said(), ["accepted"]); + assert!(cx.update(|window, _| button.is_focused(window))); } fn cookies() -> AnyElement { @@ -106,6 +179,10 @@ fn a_cookie_banner_hands_over_the_kinds_chosen(cx: &mut TestAppContext) { press("tab", cx); press("enter", cx); assert!(!at_root(cx), "Save choices took Choose's focus"); + assert!( + shown("cookie-necessary-true-true", cx), + "necessary cookies stay on" + ); for _ in 0..3 { press("shift-tab", cx); }