1use std::cell::RefCell;
15use std::future::Future;
16use std::pin::Pin;
17use std::rc::Rc;
18
19use day_spec::Cap;
20
21use crate::AnyPiece;
22
23#[derive(Clone, Debug, PartialEq, Eq)]
25pub struct Fail(pub String);
26
27impl std::fmt::Display for Fail {
28 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29 f.write_str(&self.0)
30 }
31}
32
33impl From<String> for Fail {
34 fn from(s: String) -> Self {
35 Fail(s)
36 }
37}
38
39impl From<&str> for Fail {
40 fn from(s: &str) -> Self {
41 Fail(s.to_owned())
42 }
43}
44
45pub type TestResult = Result<(), Fail>;
47
48#[derive(Clone, Debug, PartialEq)]
51pub enum DriveOp {
52 Tap(String),
53 Input(String, String),
54 Toggle(String, bool),
55 SetValue(String, f64),
56 Select(String, usize),
57 Focus(String),
58 Submit(String),
59 Navigate(String),
60 WaitIdle,
61 Pause(f64),
62 Shot(String),
64 AssertText(String, String),
65 AssertVisible(String),
66 AssertMissing(String),
67 AssertHidden(String),
69 Hover(String, Option<(f64, f64)>),
71 HoverLeave(String),
73 Pan(String, f64, f64),
75 Pinch(String, f64),
77 Drag(String, (f64, f64), (f64, f64)),
79 TapAt(String, f64, f64),
81 Key(Option<String>, String),
83 AssertAnnounced(String),
85 AssertValue(String, f64),
86 AssertOn(String, bool),
88 AssertFocused(String, bool),
89 AssertRoute(String),
90 A11yAudit(Option<String>),
91 AssertNative(String, NativeExpect),
93 AssertFrame(String, FrameExpect),
95 SamplePixel(String, f64, f64, String),
97 AssertOpenedUrl(String),
99 Activate(String, usize),
101 Reorder(String, usize, usize),
103 DeleteRow(String, usize),
105 SwipeRow(String, usize, bool, usize),
108 ScrollTo(String, String),
110 Expand(String, String, bool),
112 NavBack,
114 TreeMove(String, String, Option<String>, Option<usize>),
116 ContextMenu(String, String),
118 Menu(String),
120 ToolbarPress(String),
122 ToolbarToggle(String, bool),
124 DialogScripted(bool),
126 AssertPresented(Option<String>),
128 Respond(Option<usize>),
130 RespondText(String),
132}
133
134#[derive(Clone, Debug, Default, PartialEq)]
137pub struct FrameExpect {
138 pub width: Option<f64>,
139 pub height: Option<f64>,
140 pub x: Option<f64>,
141 pub y: Option<f64>,
142 pub relative_to: Option<String>,
144 pub tolerance: Option<f64>,
146}
147
148#[derive(Clone, Debug, Default, PartialEq)]
151pub struct NativeExpect {
152 pub text: Option<String>,
154 pub number: Option<f64>,
156 pub checked: Option<bool>,
158 pub enabled: Option<bool>,
160 pub visible: Option<bool>,
162}
163
164pub type OpFuture = Pin<Box<dyn Future<Output = TestResult>>>;
166
167pub trait DriveBackend {
171 fn run(&self, op: DriveOp) -> OpFuture;
172}
173
174#[derive(Clone)]
177pub struct Drive {
178 backend: Rc<dyn DriveBackend>,
179}
180
181impl Drive {
182 pub fn new(backend: Rc<dyn DriveBackend>) -> Self {
184 Drive { backend }
185 }
186
187 fn op(&self, op: DriveOp) -> OpFuture {
188 self.backend.run(op)
189 }
190
191 pub fn tap(&self, id: &str) -> OpFuture {
192 self.op(DriveOp::Tap(id.into()))
193 }
194 pub fn input(&self, id: &str, text: &str) -> OpFuture {
196 self.op(DriveOp::Input(id.into(), text.into()))
197 }
198 pub fn toggle(&self, id: &str, on: bool) -> OpFuture {
199 self.op(DriveOp::Toggle(id.into(), on))
200 }
201 pub fn set_value(&self, id: &str, value: f64) -> OpFuture {
202 self.op(DriveOp::SetValue(id.into(), value))
203 }
204 pub fn select(&self, id: &str, index: usize) -> OpFuture {
205 self.op(DriveOp::Select(id.into(), index))
206 }
207 pub fn focus(&self, id: &str) -> OpFuture {
208 self.op(DriveOp::Focus(id.into()))
209 }
210 pub fn submit(&self, id: &str) -> OpFuture {
211 self.op(DriveOp::Submit(id.into()))
212 }
213 pub fn navigate(&self, route: &str) -> OpFuture {
214 self.op(DriveOp::Navigate(route.into()))
215 }
216 pub fn wait_idle(&self) -> OpFuture {
218 self.op(DriveOp::WaitIdle)
219 }
220 pub fn pause(&self, secs: f64) -> OpFuture {
221 self.op(DriveOp::Pause(secs))
222 }
223 pub fn shot(&self, name: &str) -> OpFuture {
225 self.op(DriveOp::Shot(name.into()))
226 }
227 pub fn assert_text(&self, id: &str, text: &str) -> OpFuture {
228 self.op(DriveOp::AssertText(id.into(), text.into()))
229 }
230 pub fn assert_visible(&self, id: &str) -> OpFuture {
231 self.op(DriveOp::AssertVisible(id.into()))
232 }
233 pub fn assert_missing(&self, id: &str) -> OpFuture {
234 self.op(DriveOp::AssertMissing(id.into()))
235 }
236 pub fn hover(&self, id: &str, at: Option<(f64, f64)>) -> OpFuture {
238 self.op(DriveOp::Hover(id.into(), at))
239 }
240 pub fn hover_leave(&self, id: &str) -> OpFuture {
242 self.op(DriveOp::HoverLeave(id.into()))
243 }
244 pub fn pan(&self, id: &str, dx: f64, dy: f64) -> OpFuture {
246 self.op(DriveOp::Pan(id.into(), dx, dy))
247 }
248 pub fn pinch(&self, id: &str, scale: f64) -> OpFuture {
250 self.op(DriveOp::Pinch(id.into(), scale))
251 }
252 pub fn drag(&self, id: &str, from: (f64, f64), to: (f64, f64)) -> OpFuture {
254 self.op(DriveOp::Drag(id.into(), from, to))
255 }
256 pub fn tap_at(&self, id: &str, x: f64, y: f64) -> OpFuture {
258 self.op(DriveOp::TapAt(id.into(), x, y))
259 }
260 pub fn key(&self, id: Option<&str>, key: &str) -> OpFuture {
263 self.op(DriveOp::Key(id.map(str::to_owned), key.into()))
264 }
265 pub fn assert_announced(&self, text: &str) -> OpFuture {
267 self.op(DriveOp::AssertAnnounced(text.into()))
268 }
269 pub fn assert_hidden(&self, id: &str) -> OpFuture {
272 self.op(DriveOp::AssertHidden(id.into()))
273 }
274 pub fn assert_value(&self, id: &str, value: f64) -> OpFuture {
275 self.op(DriveOp::AssertValue(id.into(), value))
276 }
277 pub fn assert_on(&self, id: &str, on: bool) -> OpFuture {
279 self.op(DriveOp::AssertOn(id.into(), on))
280 }
281 pub fn assert_focused(&self, id: &str, focused: bool) -> OpFuture {
282 self.op(DriveOp::AssertFocused(id.into(), focused))
283 }
284 pub fn assert_route(&self, route: &str) -> OpFuture {
285 self.op(DriveOp::AssertRoute(route.into()))
286 }
287 pub fn a11y_audit(&self, id: Option<&str>) -> OpFuture {
290 self.op(DriveOp::A11yAudit(id.map(str::to_owned)))
291 }
292
293 pub fn assert_native(&self, id: &str, expect: NativeExpect) -> OpFuture {
298 self.op(DriveOp::AssertNative(id.into(), expect))
299 }
300 pub fn assert_frame(&self, id: &str, expect: FrameExpect) -> OpFuture {
302 self.op(DriveOp::AssertFrame(id.into(), expect))
303 }
304 pub fn assert_size(&self, id: &str, width: f64, height: f64) -> OpFuture {
306 self.assert_frame(
307 id,
308 FrameExpect {
309 width: Some(width),
310 height: Some(height),
311 ..Default::default()
312 },
313 )
314 }
315 pub fn sample_pixel(&self, id: &str, x: f64, y: f64, color: &str) -> OpFuture {
318 self.op(DriveOp::SamplePixel(id.into(), x, y, color.into()))
319 }
320 pub fn assert_opened_url(&self, url: &str) -> OpFuture {
323 self.op(DriveOp::AssertOpenedUrl(url.into()))
324 }
325 pub fn assert_enabled(&self, id: &str, enabled: bool) -> OpFuture {
327 self.assert_native(
328 id,
329 NativeExpect {
330 enabled: Some(enabled),
331 ..Default::default()
332 },
333 )
334 }
335
336 pub fn activate(&self, id: &str, index: usize) -> OpFuture {
338 self.op(DriveOp::Activate(id.into(), index))
339 }
340 pub fn reorder(&self, id: &str, from: usize, to: usize) -> OpFuture {
342 self.op(DriveOp::Reorder(id.into(), from, to))
343 }
344 pub fn delete_row(&self, id: &str, row: usize) -> OpFuture {
346 self.op(DriveOp::DeleteRow(id.into(), row))
347 }
348 pub fn swipe_row(&self, id: &str, row: usize, leading: bool, action: usize) -> OpFuture {
350 self.op(DriveOp::SwipeRow(id.into(), row, leading, action))
351 }
352 pub fn scroll_to(&self, id: &str, edge: &str) -> OpFuture {
354 self.op(DriveOp::ScrollTo(id.into(), edge.into()))
355 }
356 pub fn expand(&self, id: &str, row: &str, expanded: bool) -> OpFuture {
358 self.op(DriveOp::Expand(id.into(), row.into(), expanded))
359 }
360 pub fn nav_back(&self) -> OpFuture {
362 self.op(DriveOp::NavBack)
363 }
364 pub fn tree_move(
367 &self,
368 id: &str,
369 row: &str,
370 parent: Option<&str>,
371 index: Option<usize>,
372 ) -> OpFuture {
373 self.op(DriveOp::TreeMove(
374 id.into(),
375 row.into(),
376 parent.map(str::to_owned),
377 index,
378 ))
379 }
380
381 pub fn context_menu(&self, id: &str, item: &str) -> OpFuture {
384 self.op(DriveOp::ContextMenu(id.into(), item.into()))
385 }
386 pub fn menu(&self, item_id: &str) -> OpFuture {
388 self.op(DriveOp::Menu(item_id.into()))
389 }
390 pub fn toolbar_press(&self, item: &str) -> OpFuture {
392 self.op(DriveOp::ToolbarPress(item.into()))
393 }
394 pub fn toolbar_toggle(&self, item: &str, on: bool) -> OpFuture {
396 self.op(DriveOp::ToolbarToggle(item.into(), on))
397 }
398 pub fn dialogs_scripted(&self, scripted: bool) -> OpFuture {
402 self.op(DriveOp::DialogScripted(scripted))
403 }
404 pub fn assert_presented(&self, title: Option<&str>) -> OpFuture {
406 self.op(DriveOp::AssertPresented(title.map(str::to_owned)))
407 }
408 pub fn respond(&self, button: Option<usize>) -> OpFuture {
411 self.op(DriveOp::Respond(button))
412 }
413
414 pub fn respond_text(&self, text: &str) -> OpFuture {
416 self.op(DriveOp::RespondText(text.into()))
417 }
418
419 pub fn check(&self, ok: bool, what: &str) -> TestResult {
421 if ok { Ok(()) } else { Err(Fail(what.into())) }
422 }
423 pub fn check_eq<T: PartialEq + std::fmt::Debug>(&self, got: T, want: T) -> TestResult {
425 if got == want {
426 Ok(())
427 } else {
428 Err(Fail(format!("{got:?} ≠ expected {want:?}")))
429 }
430 }
431}
432
433#[derive(Clone, Copy, Debug, PartialEq, Eq)]
435pub enum TestKind {
436 Gui,
437 Headless,
438}
439
440type Body = Rc<dyn Fn(Drive) -> Pin<Box<dyn Future<Output = TestResult>>>>;
441
442#[derive(Clone)]
446pub struct Case {
447 name: String,
448 kind: TestKind,
449 proves: Vec<String>,
450 requires: Vec<Cap>,
451 requires_support: Vec<(String, fn() -> day_spec::Support)>,
453 page: Option<Rc<dyn Fn() -> AnyPiece>>,
454 shots: Vec<String>,
455 body: Option<Body>,
456 timeout_secs: Option<f64>,
457}
458
459impl Default for Case {
460 fn default() -> Self {
461 Case::new()
462 }
463}
464
465impl Case {
466 pub fn new() -> Self {
468 Case {
469 name: String::new(),
470 kind: TestKind::Gui,
471 proves: Vec::new(),
472 requires: Vec::new(),
473 requires_support: Vec::new(),
474 page: None,
475 shots: Vec::new(),
476 body: None,
477 timeout_secs: None,
478 }
479 }
480 pub fn headless() -> Self {
482 Case {
483 kind: TestKind::Headless,
484 ..Case::new()
485 }
486 }
487 pub fn timeout(mut self, secs: f64) -> Self {
490 self.timeout_secs = Some(secs);
491 self
492 }
493 pub fn proves(mut self, kind: &str) -> Self {
496 self.proves.push(format!("kind:{kind}"));
497 self
498 }
499 pub fn proves_cap(mut self, cap: Cap) -> Self {
501 self.proves.push(format!("cap:{cap:?}"));
502 self
503 }
504 pub fn proves_modifier(mut self, modifier: &str) -> Self {
506 self.proves.push(format!("modifier:{modifier}"));
507 self
508 }
509 pub fn proves_duty(mut self, duty: &str) -> Self {
511 self.proves.push(format!("duty:{duty}"));
512 self
513 }
514 pub fn requires(mut self, cap: Cap) -> Self {
517 self.requires.push(cap);
518 self
519 }
520 pub fn requires_support(mut self, what: &str, support: fn() -> day_spec::Support) -> Self {
524 self.requires_support.push((what.to_owned(), support));
525 self
526 }
527 pub fn missing_support(&self) -> Option<String> {
529 self.requires_support
530 .iter()
531 .find(|(_, f)| f() == day_spec::Support::Unsupported)
532 .map(|(what, _)| what.clone())
533 }
534 pub fn page<P: crate::Piece>(mut self, page: impl Fn() -> P + 'static) -> Self {
536 self.page = Some(Rc::new(move || AnyPiece::new(page())));
537 self
538 }
539 pub fn shot(mut self, name: &str) -> Self {
541 self.shots.push(name.into());
542 self
543 }
544 pub fn drive<F>(mut self, body: impl Fn(Drive) -> F + 'static) -> Self
548 where
549 F: Future<Output = TestResult> + 'static,
550 {
551 self.body = Some(Rc::new(move |d| Box::pin(body(d))));
552 self
553 }
554 pub fn run<F>(self, body: impl Fn(Drive) -> F + 'static) -> Self
556 where
557 F: Future<Output = TestResult> + 'static,
558 {
559 self.drive(body)
560 }
561
562 pub fn name(&self) -> &str {
565 &self.name
566 }
567 pub fn timeout_secs(&self) -> Option<f64> {
569 self.timeout_secs
570 }
571 pub fn kind(&self) -> TestKind {
572 self.kind
573 }
574 pub fn proves_keys(&self) -> &[String] {
576 &self.proves
577 }
578 pub fn requirements(&self) -> &[Cap] {
579 &self.requires
580 }
581 pub fn shots(&self) -> &[String] {
583 &self.shots
584 }
585 pub fn build_page(&self) -> Option<AnyPiece> {
587 self.page.as_ref().map(|p| p())
588 }
589 pub fn run_body(&self, drive: Drive) -> Pin<Box<dyn Future<Output = TestResult>>> {
591 match &self.body {
592 Some(body) => body(drive),
593 None => Box::pin(async { Ok(()) }),
594 }
595 }
596}
597
598#[derive(Clone, Copy)]
601pub struct TestFn {
602 ident: &'static str,
603 build: fn() -> Case,
604}
605
606impl TestFn {
607 pub const fn new(ident: &'static str, build: fn() -> Case) -> Self {
609 TestFn { ident, build }
610 }
611 pub fn name(&self) -> String {
613 let last = self.ident.rsplit("::").next().unwrap_or(self.ident);
614 last.trim().replace('_', "-")
615 }
616 pub fn case(&self) -> Case {
618 let mut case = (self.build)();
619 case.name = self.name();
620 case
621 }
622}
623
624mod registry;
625pub use registry::validate_registry;
626#[cfg(target_arch = "wasm32")]
627pub use registry::{Registration, install_web_registry};
628
629#[cfg(not(target_arch = "wasm32"))]
631#[linkme::distributed_slice]
632pub static TESTS: [TestFn];
633
634type HostNotify = Rc<dyn Fn()>;
636
637day_reactive::tls_slots! {
638 conformance;
639 static RUNTIME: RefCell<Vec<TestFn>> = const { RefCell::new(Vec::new()) };
640 static ACTIVE: RefCell<Option<Case>> = const { RefCell::new(None) };
643 static HOST: RefCell<Option<HostNotify>> = const { RefCell::new(None) };
645 static PAGE_PANIC: RefCell<Option<String>> = const { RefCell::new(None) };
647 static OPENED: RefCell<Option<Vec<String>>> = const { RefCell::new(None) };
649 static ANNOUNCED: RefCell<Option<Vec<String>>> = const { RefCell::new(None) };
651}
652
653pub fn set_intercepting_urls(on: bool) {
657 OPENED.with(|o| *o.borrow_mut() = on.then(Vec::new));
658 ANNOUNCED.with(|a| *a.borrow_mut() = on.then(Vec::new));
659}
660
661pub fn note_announcement(text: &str) {
665 ANNOUNCED.with(|a| {
666 if let Some(list) = a.borrow_mut().as_mut() {
667 list.push(text.to_owned());
668 }
669 });
670}
671
672pub fn announcements() -> Vec<String> {
674 ANNOUNCED.with(|a| a.borrow().clone().unwrap_or_default())
675}
676
677pub fn intercept_open_url(url: &str) -> bool {
679 OPENED.with(|o| match o.borrow_mut().as_mut() {
680 Some(list) => {
681 list.push(url.to_owned());
682 true
683 }
684 None => false,
685 })
686}
687
688pub fn opened_urls() -> Vec<String> {
690 OPENED.with(|o| o.borrow().clone().unwrap_or_default())
691}
692
693pub fn register(test: TestFn) {
695 RUNTIME.with(|r| {
696 let mut r = r.borrow_mut();
697 if !r.iter().any(|t| std::ptr::fn_addr_eq(t.build, test.build)) {
698 r.push(test);
699 }
700 });
701}
702
703pub fn tests() -> Vec<TestFn> {
705 let mut all: Vec<TestFn> = Vec::new();
706 all.extend(registry::compiled_tests());
707 RUNTIME.with(|r| {
708 for t in r.borrow().iter() {
709 if !all.iter().any(|a| std::ptr::fn_addr_eq(a.build, t.build)) {
710 all.push(*t);
711 }
712 }
713 });
714 all
715}
716
717pub fn cases() -> Vec<Case> {
719 let mut cases: Vec<Case> = tests().iter().map(TestFn::case).collect();
720 cases.sort_by(|a, b| a.name.cmp(&b.name));
721 cases
722}
723
724pub fn set_active(case: Case) -> bool {
727 ACTIVE.with(|a| *a.borrow_mut() = Some(case));
728 PAGE_PANIC.with(|p| *p.borrow_mut() = None);
729 notify_host()
730}
731
732pub fn clear_active() {
734 ACTIVE.with(|a| *a.borrow_mut() = None);
735 notify_host();
736}
737
738fn notify_host() -> bool {
739 let host = HOST.with(|h| h.borrow().clone());
740 match host {
741 Some(rebuild) => {
742 rebuild();
743 true
744 }
745 None => false,
746 }
747}
748
749pub fn active() -> Option<Case> {
751 ACTIVE.with(|a| a.borrow().clone())
752}
753
754pub fn set_host(rebuild: Option<Rc<dyn Fn()>>) {
757 HOST.with(|h| *h.borrow_mut() = rebuild);
758}
759
760pub fn note_page_panic(message: String) {
762 PAGE_PANIC.with(|p| *p.borrow_mut() = Some(message));
763}
764
765pub fn take_page_panic() -> Option<String> {
767 PAGE_PANIC.with(|p| p.borrow_mut().take())
768}
769
770pub fn panic_message(payload: &(dyn std::any::Any + Send)) -> String {
772 if let Some(s) = payload.downcast_ref::<&str>() {
773 (*s).to_owned()
774 } else if let Some(s) = payload.downcast_ref::<String>() {
775 s.clone()
776 } else {
777 "a panic with no message".to_owned()
778 }
779}
780
781pub fn case_named(name: &str) -> Option<Case> {
784 tests()
785 .into_iter()
786 .find(|t| t.name() == name)
787 .map(|t| t.case())
788}