use std::cell::RefCell;
use std::future::Future;
use std::pin::Pin;
use std::rc::Rc;
use day_spec::Cap;
use crate::AnyPiece;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Fail(pub String);
impl std::fmt::Display for Fail {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl From<String> for Fail {
fn from(s: String) -> Self {
Fail(s)
}
}
impl From<&str> for Fail {
fn from(s: &str) -> Self {
Fail(s.to_owned())
}
}
pub type TestResult = Result<(), Fail>;
#[derive(Clone, Debug, PartialEq)]
pub enum DriveOp {
Tap(String),
Input(String, String),
Toggle(String, bool),
SetValue(String, f64),
Select(String, usize),
Focus(String),
Submit(String),
Navigate(String),
WaitIdle,
Pause(f64),
Shot(String),
AssertText(String, String),
AssertVisible(String),
AssertMissing(String),
AssertHidden(String),
Hover(String, Option<(f64, f64)>),
HoverLeave(String),
Pan(String, f64, f64),
Pinch(String, f64),
Drag(String, (f64, f64), (f64, f64)),
TapAt(String, f64, f64),
Key(Option<String>, String),
AssertAnnounced(String),
AssertValue(String, f64),
AssertOn(String, bool),
AssertFocused(String, bool),
AssertRoute(String),
A11yAudit(Option<String>),
AssertNative(String, NativeExpect),
AssertFrame(String, FrameExpect),
SamplePixel(String, f64, f64, String),
AssertOpenedUrl(String),
Activate(String, usize),
Reorder(String, usize, usize),
DeleteRow(String, usize),
SwipeRow(String, usize, bool, usize),
ScrollTo(String, String),
Expand(String, String, bool),
NavBack,
TreeMove(String, String, Option<String>, Option<usize>),
ContextMenu(String, String),
Menu(String),
ToolbarPress(String),
ToolbarToggle(String, bool),
DialogScripted(bool),
AssertPresented(Option<String>),
Respond(Option<usize>),
RespondText(String),
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct FrameExpect {
pub width: Option<f64>,
pub height: Option<f64>,
pub x: Option<f64>,
pub y: Option<f64>,
pub relative_to: Option<String>,
pub tolerance: Option<f64>,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct NativeExpect {
pub text: Option<String>,
pub number: Option<f64>,
pub checked: Option<bool>,
pub enabled: Option<bool>,
pub visible: Option<bool>,
}
pub type OpFuture = Pin<Box<dyn Future<Output = TestResult>>>;
pub trait DriveBackend {
fn run(&self, op: DriveOp) -> OpFuture;
}
#[derive(Clone)]
pub struct Drive {
backend: Rc<dyn DriveBackend>,
}
impl Drive {
pub fn new(backend: Rc<dyn DriveBackend>) -> Self {
Drive { backend }
}
fn op(&self, op: DriveOp) -> OpFuture {
self.backend.run(op)
}
pub fn tap(&self, id: &str) -> OpFuture {
self.op(DriveOp::Tap(id.into()))
}
pub fn input(&self, id: &str, text: &str) -> OpFuture {
self.op(DriveOp::Input(id.into(), text.into()))
}
pub fn toggle(&self, id: &str, on: bool) -> OpFuture {
self.op(DriveOp::Toggle(id.into(), on))
}
pub fn set_value(&self, id: &str, value: f64) -> OpFuture {
self.op(DriveOp::SetValue(id.into(), value))
}
pub fn select(&self, id: &str, index: usize) -> OpFuture {
self.op(DriveOp::Select(id.into(), index))
}
pub fn focus(&self, id: &str) -> OpFuture {
self.op(DriveOp::Focus(id.into()))
}
pub fn submit(&self, id: &str) -> OpFuture {
self.op(DriveOp::Submit(id.into()))
}
pub fn navigate(&self, route: &str) -> OpFuture {
self.op(DriveOp::Navigate(route.into()))
}
pub fn wait_idle(&self) -> OpFuture {
self.op(DriveOp::WaitIdle)
}
pub fn pause(&self, secs: f64) -> OpFuture {
self.op(DriveOp::Pause(secs))
}
pub fn shot(&self, name: &str) -> OpFuture {
self.op(DriveOp::Shot(name.into()))
}
pub fn assert_text(&self, id: &str, text: &str) -> OpFuture {
self.op(DriveOp::AssertText(id.into(), text.into()))
}
pub fn assert_visible(&self, id: &str) -> OpFuture {
self.op(DriveOp::AssertVisible(id.into()))
}
pub fn assert_missing(&self, id: &str) -> OpFuture {
self.op(DriveOp::AssertMissing(id.into()))
}
pub fn hover(&self, id: &str, at: Option<(f64, f64)>) -> OpFuture {
self.op(DriveOp::Hover(id.into(), at))
}
pub fn hover_leave(&self, id: &str) -> OpFuture {
self.op(DriveOp::HoverLeave(id.into()))
}
pub fn pan(&self, id: &str, dx: f64, dy: f64) -> OpFuture {
self.op(DriveOp::Pan(id.into(), dx, dy))
}
pub fn pinch(&self, id: &str, scale: f64) -> OpFuture {
self.op(DriveOp::Pinch(id.into(), scale))
}
pub fn drag(&self, id: &str, from: (f64, f64), to: (f64, f64)) -> OpFuture {
self.op(DriveOp::Drag(id.into(), from, to))
}
pub fn tap_at(&self, id: &str, x: f64, y: f64) -> OpFuture {
self.op(DriveOp::TapAt(id.into(), x, y))
}
pub fn key(&self, id: Option<&str>, key: &str) -> OpFuture {
self.op(DriveOp::Key(id.map(str::to_owned), key.into()))
}
pub fn assert_announced(&self, text: &str) -> OpFuture {
self.op(DriveOp::AssertAnnounced(text.into()))
}
pub fn assert_hidden(&self, id: &str) -> OpFuture {
self.op(DriveOp::AssertHidden(id.into()))
}
pub fn assert_value(&self, id: &str, value: f64) -> OpFuture {
self.op(DriveOp::AssertValue(id.into(), value))
}
pub fn assert_on(&self, id: &str, on: bool) -> OpFuture {
self.op(DriveOp::AssertOn(id.into(), on))
}
pub fn assert_focused(&self, id: &str, focused: bool) -> OpFuture {
self.op(DriveOp::AssertFocused(id.into(), focused))
}
pub fn assert_route(&self, route: &str) -> OpFuture {
self.op(DriveOp::AssertRoute(route.into()))
}
pub fn a11y_audit(&self, id: Option<&str>) -> OpFuture {
self.op(DriveOp::A11yAudit(id.map(str::to_owned)))
}
pub fn assert_native(&self, id: &str, expect: NativeExpect) -> OpFuture {
self.op(DriveOp::AssertNative(id.into(), expect))
}
pub fn assert_frame(&self, id: &str, expect: FrameExpect) -> OpFuture {
self.op(DriveOp::AssertFrame(id.into(), expect))
}
pub fn assert_size(&self, id: &str, width: f64, height: f64) -> OpFuture {
self.assert_frame(
id,
FrameExpect {
width: Some(width),
height: Some(height),
..Default::default()
},
)
}
pub fn sample_pixel(&self, id: &str, x: f64, y: f64, color: &str) -> OpFuture {
self.op(DriveOp::SamplePixel(id.into(), x, y, color.into()))
}
pub fn assert_opened_url(&self, url: &str) -> OpFuture {
self.op(DriveOp::AssertOpenedUrl(url.into()))
}
pub fn assert_enabled(&self, id: &str, enabled: bool) -> OpFuture {
self.assert_native(
id,
NativeExpect {
enabled: Some(enabled),
..Default::default()
},
)
}
pub fn activate(&self, id: &str, index: usize) -> OpFuture {
self.op(DriveOp::Activate(id.into(), index))
}
pub fn reorder(&self, id: &str, from: usize, to: usize) -> OpFuture {
self.op(DriveOp::Reorder(id.into(), from, to))
}
pub fn delete_row(&self, id: &str, row: usize) -> OpFuture {
self.op(DriveOp::DeleteRow(id.into(), row))
}
pub fn swipe_row(&self, id: &str, row: usize, leading: bool, action: usize) -> OpFuture {
self.op(DriveOp::SwipeRow(id.into(), row, leading, action))
}
pub fn scroll_to(&self, id: &str, edge: &str) -> OpFuture {
self.op(DriveOp::ScrollTo(id.into(), edge.into()))
}
pub fn expand(&self, id: &str, row: &str, expanded: bool) -> OpFuture {
self.op(DriveOp::Expand(id.into(), row.into(), expanded))
}
pub fn nav_back(&self) -> OpFuture {
self.op(DriveOp::NavBack)
}
pub fn tree_move(
&self,
id: &str,
row: &str,
parent: Option<&str>,
index: Option<usize>,
) -> OpFuture {
self.op(DriveOp::TreeMove(
id.into(),
row.into(),
parent.map(str::to_owned),
index,
))
}
pub fn context_menu(&self, id: &str, item: &str) -> OpFuture {
self.op(DriveOp::ContextMenu(id.into(), item.into()))
}
pub fn menu(&self, item_id: &str) -> OpFuture {
self.op(DriveOp::Menu(item_id.into()))
}
pub fn toolbar_press(&self, item: &str) -> OpFuture {
self.op(DriveOp::ToolbarPress(item.into()))
}
pub fn toolbar_toggle(&self, item: &str, on: bool) -> OpFuture {
self.op(DriveOp::ToolbarToggle(item.into(), on))
}
pub fn dialogs_scripted(&self, scripted: bool) -> OpFuture {
self.op(DriveOp::DialogScripted(scripted))
}
pub fn assert_presented(&self, title: Option<&str>) -> OpFuture {
self.op(DriveOp::AssertPresented(title.map(str::to_owned)))
}
pub fn respond(&self, button: Option<usize>) -> OpFuture {
self.op(DriveOp::Respond(button))
}
pub fn respond_text(&self, text: &str) -> OpFuture {
self.op(DriveOp::RespondText(text.into()))
}
pub fn check(&self, ok: bool, what: &str) -> TestResult {
if ok { Ok(()) } else { Err(Fail(what.into())) }
}
pub fn check_eq<T: PartialEq + std::fmt::Debug>(&self, got: T, want: T) -> TestResult {
if got == want {
Ok(())
} else {
Err(Fail(format!("{got:?} ≠ expected {want:?}")))
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TestKind {
Gui,
Headless,
}
type Body = Rc<dyn Fn(Drive) -> Pin<Box<dyn Future<Output = TestResult>>>>;
#[derive(Clone)]
pub struct Case {
name: String,
kind: TestKind,
proves: Vec<String>,
requires: Vec<Cap>,
requires_support: Vec<(String, fn() -> day_spec::Support)>,
page: Option<Rc<dyn Fn() -> AnyPiece>>,
shots: Vec<String>,
body: Option<Body>,
timeout_secs: Option<f64>,
}
impl Default for Case {
fn default() -> Self {
Case::new()
}
}
impl Case {
pub fn new() -> Self {
Case {
name: String::new(),
kind: TestKind::Gui,
proves: Vec::new(),
requires: Vec::new(),
requires_support: Vec::new(),
page: None,
shots: Vec::new(),
body: None,
timeout_secs: None,
}
}
pub fn headless() -> Self {
Case {
kind: TestKind::Headless,
..Case::new()
}
}
pub fn timeout(mut self, secs: f64) -> Self {
self.timeout_secs = Some(secs);
self
}
pub fn proves(mut self, kind: &str) -> Self {
self.proves.push(format!("kind:{kind}"));
self
}
pub fn proves_cap(mut self, cap: Cap) -> Self {
self.proves.push(format!("cap:{cap:?}"));
self
}
pub fn proves_modifier(mut self, modifier: &str) -> Self {
self.proves.push(format!("modifier:{modifier}"));
self
}
pub fn proves_duty(mut self, duty: &str) -> Self {
self.proves.push(format!("duty:{duty}"));
self
}
pub fn requires(mut self, cap: Cap) -> Self {
self.requires.push(cap);
self
}
pub fn requires_support(mut self, what: &str, support: fn() -> day_spec::Support) -> Self {
self.requires_support.push((what.to_owned(), support));
self
}
pub fn missing_support(&self) -> Option<String> {
self.requires_support
.iter()
.find(|(_, f)| f() == day_spec::Support::Unsupported)
.map(|(what, _)| what.clone())
}
pub fn page<P: crate::Piece>(mut self, page: impl Fn() -> P + 'static) -> Self {
self.page = Some(Rc::new(move || AnyPiece::new(page())));
self
}
pub fn shot(mut self, name: &str) -> Self {
self.shots.push(name.into());
self
}
pub fn drive<F>(mut self, body: impl Fn(Drive) -> F + 'static) -> Self
where
F: Future<Output = TestResult> + 'static,
{
self.body = Some(Rc::new(move |d| Box::pin(body(d))));
self
}
pub fn run<F>(self, body: impl Fn(Drive) -> F + 'static) -> Self
where
F: Future<Output = TestResult> + 'static,
{
self.drive(body)
}
pub fn name(&self) -> &str {
&self.name
}
pub fn timeout_secs(&self) -> Option<f64> {
self.timeout_secs
}
pub fn kind(&self) -> TestKind {
self.kind
}
pub fn proves_keys(&self) -> &[String] {
&self.proves
}
pub fn requirements(&self) -> &[Cap] {
&self.requires
}
pub fn shots(&self) -> &[String] {
&self.shots
}
pub fn build_page(&self) -> Option<AnyPiece> {
self.page.as_ref().map(|p| p())
}
pub fn run_body(&self, drive: Drive) -> Pin<Box<dyn Future<Output = TestResult>>> {
match &self.body {
Some(body) => body(drive),
None => Box::pin(async { Ok(()) }),
}
}
}
#[derive(Clone, Copy)]
pub struct TestFn {
ident: &'static str,
build: fn() -> Case,
}
impl TestFn {
pub const fn new(ident: &'static str, build: fn() -> Case) -> Self {
TestFn { ident, build }
}
pub fn name(&self) -> String {
let last = self.ident.rsplit("::").next().unwrap_or(self.ident);
last.trim().replace('_', "-")
}
pub fn case(&self) -> Case {
let mut case = (self.build)();
case.name = self.name();
case
}
}
mod registry;
pub use registry::validate_registry;
#[cfg(target_arch = "wasm32")]
pub use registry::{Registration, install_web_registry};
#[cfg(not(target_arch = "wasm32"))]
#[linkme::distributed_slice]
pub static TESTS: [TestFn];
type HostNotify = Rc<dyn Fn()>;
day_reactive::tls_slots! {
conformance;
static RUNTIME: RefCell<Vec<TestFn>> = const { RefCell::new(Vec::new()) };
static ACTIVE: RefCell<Option<Case>> = const { RefCell::new(None) };
static HOST: RefCell<Option<HostNotify>> = const { RefCell::new(None) };
static PAGE_PANIC: RefCell<Option<String>> = const { RefCell::new(None) };
static OPENED: RefCell<Option<Vec<String>>> = const { RefCell::new(None) };
static ANNOUNCED: RefCell<Option<Vec<String>>> = const { RefCell::new(None) };
}
pub fn set_intercepting_urls(on: bool) {
OPENED.with(|o| *o.borrow_mut() = on.then(Vec::new));
ANNOUNCED.with(|a| *a.borrow_mut() = on.then(Vec::new));
}
pub fn note_announcement(text: &str) {
ANNOUNCED.with(|a| {
if let Some(list) = a.borrow_mut().as_mut() {
list.push(text.to_owned());
}
});
}
pub fn announcements() -> Vec<String> {
ANNOUNCED.with(|a| a.borrow().clone().unwrap_or_default())
}
pub fn intercept_open_url(url: &str) -> bool {
OPENED.with(|o| match o.borrow_mut().as_mut() {
Some(list) => {
list.push(url.to_owned());
true
}
None => false,
})
}
pub fn opened_urls() -> Vec<String> {
OPENED.with(|o| o.borrow().clone().unwrap_or_default())
}
pub fn register(test: TestFn) {
RUNTIME.with(|r| {
let mut r = r.borrow_mut();
if !r.iter().any(|t| std::ptr::fn_addr_eq(t.build, test.build)) {
r.push(test);
}
});
}
pub fn tests() -> Vec<TestFn> {
let mut all: Vec<TestFn> = Vec::new();
all.extend(registry::compiled_tests());
RUNTIME.with(|r| {
for t in r.borrow().iter() {
if !all.iter().any(|a| std::ptr::fn_addr_eq(a.build, t.build)) {
all.push(*t);
}
}
});
all
}
pub fn cases() -> Vec<Case> {
let mut cases: Vec<Case> = tests().iter().map(TestFn::case).collect();
cases.sort_by(|a, b| a.name.cmp(&b.name));
cases
}
pub fn set_active(case: Case) -> bool {
ACTIVE.with(|a| *a.borrow_mut() = Some(case));
PAGE_PANIC.with(|p| *p.borrow_mut() = None);
notify_host()
}
pub fn clear_active() {
ACTIVE.with(|a| *a.borrow_mut() = None);
notify_host();
}
fn notify_host() -> bool {
let host = HOST.with(|h| h.borrow().clone());
match host {
Some(rebuild) => {
rebuild();
true
}
None => false,
}
}
pub fn active() -> Option<Case> {
ACTIVE.with(|a| a.borrow().clone())
}
pub fn set_host(rebuild: Option<Rc<dyn Fn()>>) {
HOST.with(|h| *h.borrow_mut() = rebuild);
}
pub fn note_page_panic(message: String) {
PAGE_PANIC.with(|p| *p.borrow_mut() = Some(message));
}
pub fn take_page_panic() -> Option<String> {
PAGE_PANIC.with(|p| p.borrow_mut().take())
}
pub fn panic_message(payload: &(dyn std::any::Any + Send)) -> String {
if let Some(s) = payload.downcast_ref::<&str>() {
(*s).to_owned()
} else if let Some(s) = payload.downcast_ref::<String>() {
s.clone()
} else {
"a panic with no message".to_owned()
}
}
pub fn case_named(name: &str) -> Option<Case> {
tests()
.into_iter()
.find(|t| t.name() == name)
.map(|t| t.case())
}