Skip to content

Commit

Permalink
Add start of DOM Testing Library
Browse files Browse the repository at this point in the history
  • Loading branch information
DanielleHuisman committed Aug 15, 2024
1 parent e62ac08 commit c3e4f70
Show file tree
Hide file tree
Showing 12 changed files with 386 additions and 2 deletions.
67 changes: 67 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,8 @@ repository = "https://github.com/RustForWeb/radix"
version = "0.0.2"

[workspace.dependencies]
web-sys = "0.3.69"
regex = "1.10.6"
thiserror = "1.0.63"
web-sys = "0.3.70"
wasm-bindgen = "0.2.93"
wasm-bindgen-test = "0.3.42"
9 changes: 8 additions & 1 deletion packages/dom/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,11 @@ repository.workspace = true
version.workspace = true

[dependencies]
web-sys.workspace = true
regex.workspace = true
thiserror.workspace = true
wasm-bindgen.workspace = true
web-sys = { workspace = true, features = [
"Element",
"HtmlElement",
"NodeList",
] }
14 changes: 14 additions & 0 deletions packages/dom/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
use thiserror::Error;
use web_sys::wasm_bindgen::JsValue;

#[derive(Debug, Error)]
pub enum QueryError {
#[error("invalid configuration: {0}")]
Configuration(String),
#[error("{0:?}")]
JsError(JsValue),
#[error("{0}")]
NoElements(String),
#[error("{0}")]
MultipleElements(String),
}
10 changes: 10 additions & 0 deletions packages/dom/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1 +1,11 @@
mod error;
mod matches;
pub mod queries;
pub mod query_helpers;
mod types;
mod util;

pub use matches::get_default_normalizer;
pub use queries::*;
pub use query_helpers::*;
pub use types::*;
105 changes: 105 additions & 0 deletions packages/dom/src/matches.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
use regex::Regex;
use web_sys::Element;

use crate::{
error::QueryError, DefaultNormalizerOptions, Matcher, NormalizerFn, NormalizerOptions,
};

pub fn fuzzy_matches(
text_to_match: Option<String>,
node: Option<&Element>,
matcher: &Matcher,
normalizer: &NormalizerFn,
) -> bool {
if let Some(text_to_match) = text_to_match {
let normalized_text = normalizer(text_to_match);

match matcher {
Matcher::Function(matcher) => matcher(normalized_text, node),
Matcher::Regex(matcher) => match_regex(matcher, normalized_text),
Matcher::Number(matcher) => normalized_text == matcher.to_string(),
Matcher::String(matcher) => normalized_text.to_lowercase() == matcher.to_lowercase(),
}
} else {
false
}
}

pub fn matches(
text_to_match: Option<String>,
node: Option<&Element>,
matcher: &Matcher,
normalizer: &NormalizerFn,
) -> bool {
if let Some(text_to_match) = text_to_match {
let normalized_text = normalizer(text_to_match);

match matcher {
Matcher::Function(matcher) => matcher(normalized_text, node),
Matcher::Regex(matcher) => match_regex(matcher, normalized_text),
Matcher::Number(matcher) => normalized_text == matcher.to_string(),
Matcher::String(matcher) => normalized_text == *matcher,
}
} else {
false
}
}

pub fn get_default_normalizer(
DefaultNormalizerOptions {
trim,
collapse_whitespace,
}: DefaultNormalizerOptions,
) -> Box<NormalizerFn> {
let trim = trim.unwrap_or(true);
let collapse_whitespace = collapse_whitespace.unwrap_or(true);

Box::new(move |text| {
let mut normalized_text = text;

if trim {
normalized_text = normalized_text.trim().to_string();
}

if collapse_whitespace {
normalized_text = Regex::new(r"\s+")
.expect("Regex should be valid.")
.replace_all(&normalized_text, " ")
.to_string();
}

normalized_text
})
}

/// Constructs a normalizer to pass to matches functions.
pub fn make_normalizer(
NormalizerOptions {
trim,
collapse_whitespace,
normalizer,
}: NormalizerOptions,
) -> Result<Box<NormalizerFn>, QueryError> {
if let Some(normalizer) = normalizer {
if trim.is_some() || collapse_whitespace.is_some() {
Err(QueryError::Configuration("\n\
`trim` and `collapse_whitespace` are not supported with a normalizer. \n\
If you want to use the default trim and `collapse_whitespace logic in your normalizer, \n\
use `get_default_normalizer(DefaultNormalizerOptions {trim, collapse_whitespace})` and compose that into your normalizer.\
".into()))
} else {
Ok(normalizer)
}
} else {
// No custom normalizer specified. Just use default.
Ok(get_default_normalizer(DefaultNormalizerOptions {
trim,
collapse_whitespace,
}))
}
}

fn match_regex(matcher: &Regex, text: String) -> bool {
// TODO: if statement?
matcher.is_match(&text)
}
3 changes: 3 additions & 0 deletions packages/dom/src/queries.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mod alt_text;

pub use alt_text::*;
33 changes: 33 additions & 0 deletions packages/dom/src/queries/alt_text.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use regex::Regex;
use web_sys::{Element, HtmlElement};

use crate::{
error::QueryError,
query_all_by_attribute,
types::{Matcher, MatcherOptions},
};

pub fn query_all_by_alt_text(
container: HtmlElement,
alt: &Matcher,
options: MatcherOptions,
) -> Result<Vec<HtmlElement>, QueryError> {
// check_container_type(container);

let valid_tag_regex = Regex::new(r"^(img|input|area|.+-.+)$").expect("Regex should be valid.");

Ok(
query_all_by_attribute("alt".to_string(), container, alt, options)?
.into_iter()
.filter(|node| valid_tag_regex.is_match(&node.tag_name()))
.collect(),
)
}

fn _get_multiple_error(_c: Option<Element>, alt: String) -> String {
format!("Found multiple elements with alt text: {alt}")
}

fn _get_missing_error(_c: Option<Element>, alt: String) -> String {
format!("Unable to find an element with alt text: {alt}")
}
65 changes: 65 additions & 0 deletions packages/dom/src/query_helpers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use web_sys::HtmlElement;

use crate::{
error::QueryError,
matches::{fuzzy_matches, make_normalizer, matches},
util::node_list_to_vec,
Matcher, MatcherOptions, NormalizerOptions,
};

pub fn query_all_by_attribute(
attribute: String,
container: HtmlElement,
text: &Matcher,
MatcherOptions {
exact,
trim,
collapse_whitespace,
normalizer,
..
}: MatcherOptions,
) -> Result<Vec<HtmlElement>, QueryError> {
let exact = exact.unwrap_or(true);

let matcher = match exact {
true => matches,
false => fuzzy_matches,
};
let match_normalizer = make_normalizer(NormalizerOptions {
trim,
collapse_whitespace,
normalizer,
})?;

Ok(node_list_to_vec::<HtmlElement>(
container
.query_selector_all(&format!("[{attribute}]"))
.map_err(QueryError::JsError)?,
)
.into_iter()
.filter(|node| {
matcher(
node.get_attribute(&attribute),
Some(node),
text,
match_normalizer.as_ref(),
)
})
.collect())
}

pub fn query_by_attribute(
attribute: String,
container: HtmlElement,
text: &Matcher,
options: MatcherOptions,
) -> Result<Option<HtmlElement>, QueryError> {
let mut els = query_all_by_attribute(attribute.clone(), container, text, options)?;
if els.len() > 1 {
Err(QueryError::MultipleElements(format!(
"Found multiple elements by [{attribute}={text}]"
)))
} else {
Ok(els.pop())
}
}
3 changes: 3 additions & 0 deletions packages/dom/src/types.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mod matches;

pub use matches::*;
Loading

0 comments on commit c3e4f70

Please sign in to comment.