Misc · Codes and checks: a QR code scanner and a captcha
misc::QrCodeScanner reads QR codes with rqrr 0.11 from the frames the host hands in, one at a time off the main thread, and hands each code to the owner once. misc::Captcha shows the owner's picture over an answer field that empties and keeps focus with each answer; the owner says how it stood. BarcodeDisplay, Signature and Rating Widget point to their homes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
854f44ed6c
commit
621c8406bb
14 files changed
+785
-10
No files matched your search
@@ -0,0 +1,135 @@
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use gpui::{AnyElement, IntoElement, RenderImage, TestAppContext, VisualTestContext};
|
||||
|
||||
use super::{at_root, press, settle, shown, stage};
|
||||
use crate::misc::{Captcha, CaptchaState, QrCodeScanner, scanner::frame_of};
|
||||
|
||||
thread_local! {
|
||||
static FRAME: RefCell<Option<Arc<RenderImage>>> = const { RefCell::new(None) };
|
||||
static SAID: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
|
||||
static STATE: Cell<CaptchaState> = const { Cell::new(CaptchaState::Asking) };
|
||||
}
|
||||
|
||||
fn said() -> Vec<String> {
|
||||
SAID.with(|said| said.borrow().clone())
|
||||
}
|
||||
|
||||
fn say(text: &str) {
|
||||
SAID.with(|said| said.borrow_mut().push(text.to_string()));
|
||||
}
|
||||
|
||||
/// Hands the stage a new frame, or state, and lets it draw and read.
|
||||
fn show(cx: &mut VisualTestContext) {
|
||||
cx.update(|window, _| window.refresh());
|
||||
settle(cx);
|
||||
}
|
||||
|
||||
fn scanner() -> AnyElement {
|
||||
let scanner = QrCodeScanner::new("scanner", 1.0).on_scan(|text, _, _| say(text));
|
||||
match FRAME.with(|frame| frame.borrow().clone()) {
|
||||
Some(frame) => scanner.frame(frame),
|
||||
None => scanner,
|
||||
}
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// Frames read off the main thread; a code goes to the owner once, and a new one after it.
|
||||
#[gpui::test]
|
||||
fn a_scanner_reads_each_code_once(cx: &mut TestAppContext) {
|
||||
FRAME.with(|frame| frame.replace(Some(frame_of("first"))));
|
||||
let (_, cx) = stage(scanner, cx);
|
||||
assert_eq!(said(), ["first"]);
|
||||
assert!(shown("scanned-first", cx));
|
||||
FRAME.with(|frame| frame.replace(Some(frame_of("first"))));
|
||||
show(cx);
|
||||
assert_eq!(
|
||||
said(),
|
||||
["first"],
|
||||
"a new frame of the same code says nothing"
|
||||
);
|
||||
FRAME.with(|frame| frame.replace(Some(frame_of("second"))));
|
||||
show(cx);
|
||||
assert_eq!(said(), ["first", "second"]);
|
||||
}
|
||||
|
||||
/// A frame that comes while one is read waits: the read's end redraws, and the newest frame is read next.
|
||||
#[gpui::test]
|
||||
fn a_frame_that_comes_mid_read_is_read_next(cx: &mut TestAppContext) {
|
||||
let (_, cx) = stage(scanner, cx);
|
||||
for text in ["first", "second"] {
|
||||
FRAME.with(|frame| frame.replace(Some(frame_of(text))));
|
||||
cx.update(|window, _| window.refresh());
|
||||
}
|
||||
assert!(said().is_empty(), "both frames drew before any read ended");
|
||||
cx.run_until_parked();
|
||||
assert_eq!(said(), ["first", "second"], "only the read's end redrew");
|
||||
}
|
||||
|
||||
fn captcha() -> AnyElement {
|
||||
Captcha::new("captcha", frame_of("W7XK"), 1.0)
|
||||
.state(STATE.get())
|
||||
.on_answer(|text, _, _| say(text))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// Enter hands the answer over and empties the field, which keeps focus through the owner's verdict.
|
||||
#[gpui::test]
|
||||
fn a_captcha_hands_over_answers_and_keeps_focus(cx: &mut TestAppContext) {
|
||||
let (_, cx) = stage(captcha, cx);
|
||||
press("tab", cx);
|
||||
press("enter", cx);
|
||||
assert!(said().is_empty(), "nothing typed, nothing handed over");
|
||||
cx.simulate_input(" w7xk ");
|
||||
press("enter", cx);
|
||||
assert_eq!(said(), ["w7xk"]);
|
||||
STATE.set(CaptchaState::Wrong);
|
||||
show(cx);
|
||||
assert!(shown("captcha-wrong", cx));
|
||||
cx.simulate_input("W7XK");
|
||||
press("enter", cx);
|
||||
assert_eq!(
|
||||
said(),
|
||||
["w7xk", "W7XK"],
|
||||
"the field emptied for the next try"
|
||||
);
|
||||
STATE.set(CaptchaState::Passed);
|
||||
show(cx);
|
||||
assert!(
|
||||
shown("captcha-verified", cx) && !at_root(cx),
|
||||
"the field kept focus"
|
||||
);
|
||||
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.
|
||||
#[gpui::test]
|
||||
fn verify_hands_focus_to_the_field(cx: &mut TestAppContext) {
|
||||
let (_, cx) = stage(captcha, cx);
|
||||
press("tab", cx);
|
||||
cx.simulate_input("W7XK");
|
||||
press("tab", cx);
|
||||
press("enter", cx);
|
||||
assert_eq!(said(), ["W7XK"]);
|
||||
assert!(!at_root(cx), "focus went to the field, not the root");
|
||||
}
|
||||
|
||||
/// Verify spins while the owner checks, and its mark keeps its focus once passed.
|
||||
#[gpui::test]
|
||||
fn verify_keeps_its_focus_through_the_check(cx: &mut TestAppContext) {
|
||||
let (_, cx) = stage(captcha, cx);
|
||||
press("tab", cx);
|
||||
cx.simulate_input("W7XK");
|
||||
press("tab", cx);
|
||||
STATE.set(CaptchaState::Checking);
|
||||
show(cx);
|
||||
assert!(!at_root(cx), "a spinning Verify stays a stop");
|
||||
STATE.set(CaptchaState::Passed);
|
||||
show(cx);
|
||||
assert!(!at_root(cx), "the Verified mark took Verify's focus");
|
||||
}
|
||||
Reference in new issue
Block a user