diff --git a/src/misc/captcha.rs b/src/misc/captcha.rs index 61a1ff4..ba1d7e7 100644 --- a/src/misc/captcha.rs +++ b/src/misc/captcha.rs @@ -21,15 +21,15 @@ type OnRefresh = Rc; pub enum CaptchaState { #[default] Asking, - /// The owner checks the answer: Verify spins and takes no press. + /// The owner checks the answer: Verify spins and takes no press, and Enter hands nothing over. Checking, /// The answer did not match. Wrong, - /// The field rests, and Verify becomes a mark that keeps its focus. + /// The field rests, and Verify becomes a mark that takes its focus and the field's. Passed, } -/// A picture of characters to copy, from the owner, over a field to type them. Verify or Enter hands the answer to the owner, who checks it and says how it stood; the field empties with each answer and keeps focus. With `on_refresh`, a button asks the owner for a new picture. +/// A picture of characters to copy, from the owner, over a field to type them. Verify or Enter hands the answer to the owner, who checks it and says how it stood; the field empties with each answer and keeps focus, and Verify with nothing typed sends focus to it. With `on_refresh`, a button asks the owner for a new picture. #[derive(IntoElement)] pub struct Captcha { id: ElementId, @@ -88,12 +88,20 @@ impl RenderOnce for Captcha { field.update(cx, |field, cx| field.set_disabled(passed, cx)); } let verify = tab_stop((id.clone(), "verify").into(), !passed, window, cx); - let empty = field.read(cx).text().trim().is_empty(); + if passed && field.read(cx).focus().is_focused(window) { + log::info!("captcha: passed; focus goes to the mark"); + window.focus(&verify); + } + let checking = self.state == CaptchaState::Checking; let answer = { let field = field.clone(); Rc::new(move |window: &mut Window, cx: &mut App| { + if checking { + return; + } let text = SharedString::from(field.read(cx).text().trim().to_string()); if text.is_empty() { + window.focus(field.read(cx).focus()); return; } log::info!("captcha: answered"); @@ -108,7 +116,10 @@ impl RenderOnce for Captcha { let action = match passed { true => div() .id((id.clone(), "verified")) - .debug_selector(|| "captcha-verified".into()) + .debug_selector(|| match verify.is_focused(window) { + true => "captcha-verified-focused".into(), + false => "captcha-verified".into(), + }) .track_focus(&verify) .flex() .flex_none() @@ -133,8 +144,7 @@ impl RenderOnce for Captcha { Button::new((id.clone(), "verify-button"), "Verify") .variant(ButtonVariant::Primary) .focus_handle(&verify) - .loading(self.state == CaptchaState::Checking) - .disabled(empty && self.state != CaptchaState::Checking) + .loading(checking) .on_click(move |_, window, cx| answer(window, cx)) .into_any_element() } diff --git a/src/misc/tests/codes.rs b/src/misc/tests/codes.rs index 8caaff4..5505e10 100644 --- a/src/misc/tests/codes.rs +++ b/src/misc/tests/codes.rs @@ -99,15 +99,62 @@ fn a_captcha_hands_over_answers_and_keeps_focus(cx: &mut TestAppContext) { STATE.set(CaptchaState::Passed); show(cx); assert!( - shown("captcha-verified", cx) && !at_root(cx), - "the field kept focus" + shown("captcha-verified-focused", cx), + "the resting field handed its focus to the mark" ); cx.simulate_input("again"); press("enter", cx); assert_eq!(said().len(), 2, "a passed captcha takes no more answers"); } -/// Enter on Verify hands the answer over and focus to the emptied field, as Verify rests. +/// With the field emptied, verdicts and a new ask turn Verify and its mark into each other on one focus handle. +#[gpui::test] +fn the_owners_verdicts_leave_focus_in_place(cx: &mut TestAppContext) { + let (_, cx) = stage(captcha, cx); + press("tab", cx); + cx.simulate_input("W7XK"); + press("enter", cx); + STATE.set(CaptchaState::Checking); + show(cx); + press("tab", cx); + STATE.set(CaptchaState::Wrong); + show(cx); + assert!(!at_root(cx), "Verify stayed a stop through the verdict"); + STATE.set(CaptchaState::Passed); + show(cx); + STATE.set(CaptchaState::Asking); + show(cx); + assert!(!at_root(cx), "the mark's focus went back to Verify"); +} + +/// Enter hands nothing over while the owner checks. +#[gpui::test] +fn enter_waits_while_the_owner_checks(cx: &mut TestAppContext) { + let (_, cx) = stage(captcha, cx); + press("tab", cx); + cx.simulate_input("W7XK"); + press("enter", cx); + STATE.set(CaptchaState::Checking); + show(cx); + cx.simulate_input("again"); + press("enter", cx); + assert_eq!(said(), ["W7XK"]); +} + +/// Verify with nothing typed sends focus to the field. +#[gpui::test] +fn an_empty_verify_sends_focus_to_the_field(cx: &mut TestAppContext) { + let (_, cx) = stage(captcha, cx); + press("tab", cx); + press("tab", cx); + press("enter", cx); + assert!(said().is_empty()); + cx.simulate_input("W7XK"); + press("enter", cx); + assert_eq!(said(), ["W7XK"], "the typing reached the field"); +} + +/// Enter on Verify hands the answer over and focus to the emptied field. #[gpui::test] fn verify_hands_focus_to_the_field(cx: &mut TestAppContext) { let (_, cx) = stage(captcha, cx);