add password field

This commit is contained in:
2026-11-01 17:26:30 +07:00
parent b2ce60f0e6
commit acf907461b
9 changed files with 430 additions and 56 deletions
+143
View File
@@ -590,3 +590,146 @@ fn enter_emits_submitted() {
// The content is left in place for the subscriber to read or clear.
assert_eq!(value(&cx, &state), "draft");
}
fn password_window(initial: &str) -> (TestAppContext, Entity<InputState>, VisualTestContext) {
let (cx, state, visual) = input_window(initial, false);
cx.update(|cx| {
state.update(cx, |state, cx| state.set_password(true, cx));
});
(cx, state, visual)
}
#[test]
fn password_fields_keep_the_real_buffer_while_masked() {
let (cx, state, mut visual) = password_window("");
focus(&mut visual, &state);
visual.simulate_keystrokes("s e c r e t");
// The buffer holds the real secret; masking is paint-only.
assert_eq!(value(&cx, &state), "secret");
// Editing still works through the mask.
visual.simulate_keystrokes("backspace");
assert_eq!(value(&cx, &state), "secre");
}
#[test]
fn password_fields_refuse_copy_and_cut() {
let (cx, state, mut visual) = password_window("secret");
focus(&mut visual, &state);
// Select everything and try to copy.
visual.simulate_keystrokes("cmd-a");
visual.simulate_keystrokes("cmd-c");
let clipboard = cx.update(|cx| {
cx.read_from_clipboard()
.and_then(|item| item.text())
.unwrap_or_default()
});
assert_eq!(clipboard, "");
// Cut neither leaks the secret nor deletes it.
visual.simulate_keystrokes("cmd-x");
assert_eq!(value(&cx, &state), "secret");
let clipboard = cx.update(|cx| {
cx.read_from_clipboard()
.and_then(|item| item.text())
.unwrap_or_default()
});
assert_eq!(clipboard, "");
}
#[test]
fn password_fields_still_allow_paste() {
let (cx, state, mut visual) = password_window("");
focus(&mut visual, &state);
cx.update(|cx| {
cx.write_to_clipboard(gpui::ClipboardItem::new_string("hunter2".to_string()));
});
visual.simulate_keystrokes("cmd-v");
assert_eq!(value(&cx, &state), "hunter2");
}
#[test]
fn clicks_in_password_fields_map_to_real_offsets() {
let (cx, state, mut visual) = password_window("abcdefgh");
focus(&mut visual, &state);
// Clicking at the text origin moves the caret to the start, so typing
// lands there: the masked display maps clicks back onto real offsets.
click(&mut visual, point(px(12.), px(15.)));
visual.simulate_keystrokes("x");
assert_eq!(value(&cx, &state), "xabcdefgh");
// Clicking past the last bullet clamps to the end.
click(&mut visual, point(px(500.), px(15.)));
visual.simulate_keystrokes("y");
assert_eq!(value(&cx, &state), "xabcdefghy");
}
fn click(visual: &mut VisualTestContext, position: gpui::Point<gpui::Pixels>) {
visual.simulate_mouse_down(position, MouseButton::Left, Modifiers::default());
visual.simulate_mouse_up(position, MouseButton::Left, gpui::Modifiers::default());
}
struct RevealHarness {
state: Entity<InputState>,
}
impl Render for RevealHarness {
fn render(
&mut self,
_window: &mut Window,
_cx: &mut gpui::Context<Self>,
) -> impl gpui::IntoElement {
div()
.w(px(400.))
.p_4()
.child(Input::new("reveal-input", self.state.clone()).placeholder("Password"))
}
}
fn reveal_window() -> (TestAppContext, Entity<InputState>, VisualTestContext) {
let mut cx = TestAppContext::single();
cx.update(|cx| {
Theme::init(cx);
bind_input_keys(cx, InputBindings::default());
});
let state = cx.update(|cx| InputState::new_with_text("", cx));
cx.update(|cx| {
state.update(cx, |state, cx| state.set_password(true, cx));
});
let state_for_window = state.clone();
let window = cx.add_window(move |_, _| RevealHarness {
state: state_for_window,
});
let mut visual = VisualTestContext::from_window(window.into(), &cx);
visual.update(|window, cx| {
window.draw(cx).clear(cx);
});
(cx, state, visual)
}
/// Clicking the reveal toggle flips masking on and off, so apps can reveal
/// a secret without a custom control.
#[test]
fn the_reveal_toggle_flips_masking() {
let (cx, state, mut visual) = reveal_window();
let bounds = visual
.debug_bounds("input-reveal")
.expect("reveal toggle missing");
let center = point(
bounds.left() + bounds.size.width / 2.0,
bounds.top() + bounds.size.height / 2.0,
);
// Masked: clicking reveals.
click(&mut visual, center);
let revealed = cx.update(|cx| state.read(cx).is_revealed());
assert!(revealed, "the toggle should reveal the field");
// Revealed: clicking masks again.
click(&mut visual, center);
let revealed = cx.update(|cx| state.read(cx).is_revealed());
assert!(!revealed, "the toggle should mask the field again");
}