#![cfg(target_arch = "wasm32")]
use dominator::html;
use dwind::prelude::*;
use dwind_macros::{dwclass, dwkeyframes};
use wasm_bindgen_test::*;
use web_sys::js_sys;
use web_sys::wasm_bindgen::JsCast;
wasm_bindgen_test_configure!(run_in_browser);
dwkeyframes! {
#![prefix = "dwuitest"]
#[animation("1s linear infinite")]
slide_probe {
"from" => "transform: translateX(0);",
"to" => "transform: translateX(10px);",
}
unused_probe {
"from" => "opacity: 0;",
"to" => "opacity: 1;",
}
#[animation("1s linear infinite")]
hover_probe {
"from" => "opacity: 0.4;",
"to" => "opacity: 1;",
}
#[animation("1s linear infinite")]
before_probe {
"from" => "opacity: 0.4;",
"to" => "opacity: 1;",
}
}
struct TestContainer {
element: web_sys::Element,
}
impl TestContainer {
fn new() -> Self {
let doc = web_sys::window().unwrap().document().unwrap();
let el = doc.create_element("div").unwrap();
el.set_attribute(
"style",
"position:absolute;left:0;top:0;width:800px;height:600px",
)
.unwrap();
doc.body().unwrap().append_child(&el).unwrap();
Self { element: el }
}
fn dom_element(&self) -> web_sys::HtmlElement {
self.element.clone().dyn_into().unwrap()
}
}
impl Drop for TestContainer {
fn drop(&mut self) {
self.element.remove();
}
}
async fn wait_frame() {
let promise = js_sys::Promise::new(&mut |resolve, _| {
web_sys::window()
.unwrap()
.request_animation_frame(&resolve)
.unwrap();
});
wasm_bindgen_futures::JsFuture::from(promise).await.unwrap();
}
fn count_keyframes(name: &str) -> usize {
let doc = web_sys::window().unwrap().document().unwrap();
let sheets = doc.style_sheets();
let mut found = 0;
for i in 0..sheets.length() {
let Some(sheet) = sheets.item(i) else {
continue;
};
let Ok(sheet) = sheet.dyn_into::<web_sys::CssStyleSheet>() else {
continue;
};
let Ok(rules) = sheet.css_rules() else {
continue;
};
for r in 0..rules.length() {
let Some(rule) = rules.item(r) else { continue };
if let Ok(keyframes) = rule.dyn_into::<web_sys::CssKeyframesRule>() {
if keyframes.name() == name {
found += 1;
}
}
}
}
found
}
fn computed_pseudo(element: &web_sys::Element, pseudo: &str, property: &str) -> String {
web_sys::window()
.unwrap()
.get_computed_style_with_pseudo_elt(element, pseudo)
.unwrap()
.unwrap()
.get_property_value(property)
.unwrap()
}
#[wasm_bindgen_test]
async fn keyframes_are_injected_lazily_and_only_once() {
assert_eq!(
count_keyframes("dwuitest-unused-probe"),
0,
"an unused keyframe was injected"
);
let tc = TestContainer::new();
dominator::append_dom(
&tc.dom_element(),
html!("div", {
.dwclass!("animate-slide-probe")
.child(html!("span", { .dwclass!("animate-slide-probe") }))
}),
);
wait_frame().await;
assert_eq!(
count_keyframes("dwuitest-slide-probe"),
1,
"expected exactly one @keyframes rule after two instantiations"
);
}
#[wasm_bindgen_test]
async fn keyframe_handles_register_when_formatted() {
let shorthand = format!("{UNUSED_PROBE_KEYFRAMES} 1s linear");
assert!(
shorthand.starts_with("dwuitest-unused-probe"),
"{shorthand}"
);
assert_eq!(count_keyframes("dwuitest-unused-probe"), 1);
}
#[wasm_bindgen_test]
async fn dwui_keyframes_survived_the_migration() {
dwui::theme::apply_style_sheet(None);
dwui::theme::apply_style_sheet(None);
for name in [
"dwui-modal-in",
"dwui-fade-in",
"dwui-progress-indeterminate",
"dwui-spin",
"dwui-skeleton-pulse",
] {
assert_eq!(count_keyframes(name), 1, "{name} should be injected once");
}
}
#[wasm_bindgen_test]
async fn modified_animation_utilities_still_register_their_keyframes() {
assert_eq!(count_keyframes("dwuitest-hover-probe"), 0);
assert_eq!(count_keyframes("dwuitest-before-probe"), 0);
let tc = TestContainer::new();
dominator::append_dom(
&tc.dom_element(),
html!("div", {
.dwclass!("hover:animate-hover-probe")
.child(html!("span", { .dwclass!("relative [&::before]:animate-before-probe") }))
}),
);
wait_frame().await;
assert_eq!(
count_keyframes("dwuitest-hover-probe"),
1,
"hover:animate-* did not inject its @keyframes"
);
assert_eq!(
count_keyframes("dwuitest-before-probe"),
1,
"[&::before]:animate-* did not inject its @keyframes"
);
}
#[wasm_bindgen_test]
async fn pseudo_element_variants_materialise_without_a_content_utility() {
let tc = TestContainer::new();
dominator::append_dom(
&tc.dom_element(),
html!("div", {
.attr("id", "probe-before")
.dwclass!("relative [&::before]:absolute [&::before]:opacity-100")
}),
);
wait_frame().await;
let doc = web_sys::window().unwrap().document().unwrap();
let el = doc.get_element_by_id("probe-before").unwrap();
let content = computed_pseudo(&el, "::before", "content");
assert!(
content != "none",
"::before had no generated content (got {content:?})"
);
assert_eq!(computed_pseudo(&el, "::before", "position"), "absolute");
}
#[wasm_bindgen_test]
async fn before_shorthand_matches_the_bracket_form() {
let tc = TestContainer::new();
dominator::append_dom(
&tc.dom_element(),
html!("div", {
.attr("id", "probe-alias")
.dwclass!("relative before:absolute")
}),
);
wait_frame().await;
let doc = web_sys::window().unwrap().document().unwrap();
let el = doc.get_element_by_id("probe-alias").unwrap();
assert!(computed_pseudo(&el, "::before", "content") != "none");
assert_eq!(computed_pseudo(&el, "::before", "position"), "absolute");
}
#[wasm_bindgen_test]
async fn explicit_pseudo_content_survives_composition() {
let tc = TestContainer::new();
dominator::append_dom(
&tc.dom_element(),
html!("div", {
.attr("id", "probe-compose")
.dwclass!("relative before:[content:'x'] before:absolute before:opacity-100")
}),
);
wait_frame().await;
let doc = web_sys::window().unwrap().document().unwrap();
let el = doc.get_element_by_id("probe-compose").unwrap();
let content = computed_pseudo(&el, "::before", "content");
assert!(
content.contains('x'),
"composed pseudo-element lost its content (got {content:?})"
);
assert_eq!(computed_pseudo(&el, "::before", "position"), "absolute");
}
#[wasm_bindgen_test]
async fn arbitrary_values_may_be_non_ascii() {
let tc = TestContainer::new();
dominator::append_dom(
&tc.dom_element(),
html!("div", {
.attr("id", "probe-unicode")
.dwclass!("relative before:[content:'→'] before:m-r-2")
}),
);
wait_frame().await;
let doc = web_sys::window().unwrap().document().unwrap();
let el = doc.get_element_by_id("probe-unicode").unwrap();
let content = computed_pseudo(&el, "::before", "content");
assert!(content.contains('→'), "got {content:?}");
assert_eq!(computed_pseudo(&el, "::before", "margin-right"), "8px");
}
#[wasm_bindgen_test]
async fn quoted_values_may_contain_brackets() {
let tc = TestContainer::new();
dominator::append_dom(
&tc.dom_element(),
html!("div", {
.attr("id", "probe-bracket")
.dwclass!("relative before:[content:'['] before:absolute")
}),
);
wait_frame().await;
let doc = web_sys::window().unwrap().document().unwrap();
let el = doc.get_element_by_id("probe-bracket").unwrap();
let content = computed_pseudo(&el, "::before", "content");
assert!(content.contains('['), "got {content:?}");
assert_eq!(computed_pseudo(&el, "::before", "position"), "absolute");
}
#[wasm_bindgen_test]
async fn pseudo_classes_do_not_gain_content() {
let tc = TestContainer::new();
dominator::append_dom(
&tc.dom_element(),
html!("div", {
.attr("id", "probe-hover")
.dwclass!("hover:opacity-50")
}),
);
wait_frame().await;
let doc = web_sys::window().unwrap().document().unwrap();
let el = doc.get_element_by_id("probe-hover").unwrap();
assert_eq!(computed_pseudo(&el, "::before", "content"), "none");
}
#[wasm_bindgen_test]
async fn arbitrary_declarations_reach_the_element() {
let tc = TestContainer::new();
dominator::append_dom(
&tc.dom_element(),
html!("div", {
.attr("id", "probe-arbitrary")
.dwclass!("[mix-blend-mode:overlay] [--sx:42%] [letter-spacing:0.5px]")
}),
);
wait_frame().await;
let doc = web_sys::window().unwrap().document().unwrap();
let el = doc.get_element_by_id("probe-arbitrary").unwrap();
let style = web_sys::window()
.unwrap()
.get_computed_style(&el)
.unwrap()
.unwrap();
assert_eq!(
style.get_property_value("mix-blend-mode").unwrap(),
"overlay"
);
assert_eq!(style.get_property_value("letter-spacing").unwrap(), "0.5px");
assert_eq!(style.get_property_value("--sx").unwrap().trim(), "42%");
}
#[wasm_bindgen_test]
async fn arbitrary_declarations_compose_with_pseudo_elements() {
let tc = TestContainer::new();
dominator::append_dom(
&tc.dom_element(),
html!("div", {
.attr("id", "probe-arb-before")
.dwclass!("relative [&::before]:[mix-blend-mode:screen]")
}),
);
wait_frame().await;
let doc = web_sys::window().unwrap().document().unwrap();
let el = doc.get_element_by_id("probe-arb-before").unwrap();
assert!(computed_pseudo(&el, "::before", "content") != "none");
assert_eq!(computed_pseudo(&el, "::before", "mix-blend-mode"), "screen");
}
#[wasm_bindgen_test]
async fn newly_added_utilities_apply() {
let tc = TestContainer::new();
dominator::append_dom(
&tc.dom_element(),
html!("div", {
.attr("id", "probe-utils")
.dwclass!("absolute inset-0 isolate no-underline whitespace-nowrap will-change-transform delay-150 tracking-tight")
}),
);
wait_frame().await;
let doc = web_sys::window().unwrap().document().unwrap();
let el = doc.get_element_by_id("probe-utils").unwrap();
let style = web_sys::window()
.unwrap()
.get_computed_style(&el)
.unwrap()
.unwrap();
assert_eq!(style.get_property_value("isolation").unwrap(), "isolate");
assert_eq!(style.get_property_value("white-space").unwrap(), "nowrap");
assert_eq!(
style.get_property_value("transition-delay").unwrap(),
"0.15s"
);
assert_eq!(style.get_property_value("top").unwrap(), "0px");
assert_eq!(
style.get_property_value("text-decoration-line").unwrap(),
"none"
);
}
#[wasm_bindgen_test]
async fn pseudo_content_composes_in_either_order() {
let tc = TestContainer::new();
dominator::append_dom(
&tc.dom_element(),
html!("div", {
.child(html!("div", {
.attr("id", "order-a")
.dwclass!("relative before:[content:'a'] before:absolute")
}))
.child(html!("div", {
.attr("id", "order-b")
.dwclass!("relative before:absolute before:[content:'b']")
}))
}),
);
wait_frame().await;
let doc = web_sys::window().unwrap().document().unwrap();
for (id, want) in [("order-a", 'a'), ("order-b", 'b')] {
let el = doc.get_element_by_id(id).unwrap();
let content = computed_pseudo(&el, "::before", "content");
assert!(content.contains(want), "{id}: got {content:?}");
assert_eq!(computed_pseudo(&el, "::before", "position"), "absolute");
}
}
#[wasm_bindgen_test]
async fn content_utilities_reach_the_pseudo_element() {
let tc = TestContainer::new();
dominator::append_dom(
&tc.dom_element(),
html!("div", {
.attr("id", "probe-content-none")
.dwclass!("relative before:absolute before:content-none")
}),
);
wait_frame().await;
let doc = web_sys::window().unwrap().document().unwrap();
let el = doc.get_element_by_id("probe-content-none").unwrap();
assert_eq!(computed_pseudo(&el, "::before", "content"), "none");
}
dwind_macros::dwgenerate!("aliased-flex", "flex");
dwind_macros::dwgenerate!("aliased-anim", "animate-slide-probe");
#[wasm_bindgen_test]
async fn dwgenerate_can_alias_a_plain_class_and_an_animation() {
let tc = TestContainer::new();
dominator::append_dom(
&tc.dom_element(),
html!("div", {
.attr("id", "probe-alias")
.dwclass!("aliased-flex aliased-anim")
}),
);
wait_frame().await;
let doc = web_sys::window().unwrap().document().unwrap();
let el = doc.get_element_by_id("probe-alias").unwrap();
let style = web_sys::window()
.unwrap()
.get_computed_style(&el)
.unwrap()
.unwrap();
assert_eq!(style.get_property_value("display").unwrap(), "flex");
assert!(
style
.get_property_value("animation-name")
.unwrap()
.contains("slide-probe"),
"{:?}",
style.get_property_value("animation-name")
);
assert_eq!(count_keyframes("dwuitest-slide-probe"), 1);
}
#[wasm_bindgen_test]
async fn pointer_spotlight_selectors_insert() {
let tc = TestContainer::new();
dominator::append_dom(
&tc.dom_element(),
html!("div", {
.dwclass!("relative isolate [transition:transform 400ms cubic-bezier(0.16, 1, 0.3, 1), border-color 300ms ease]")
.dwclass!("[&::before]:absolute [&::before]:inset-0 [&::before]:[z-index:-1] \
[&::before]:[border-radius:inherit] [&::before]:opacity-0 \
[&::before]:[transition:opacity 320ms ease] [&.hot::before]:opacity-100 \
[&::before]:[background:radial-gradient(22rem circle at var(--sx, 50%) var(--sy, 50%), rgba(213, 182, 95, 0.13), transparent 62%)]")
.dwclass!("[&::after]:absolute [&::after]:inset-0 [&::after]:[z-index:-1] \
[&::after]:[border-radius:inherit] [&::after]:[padding:1px] \
[&::after]:opacity-0 [&::after]:[transition:opacity 320ms ease] [&.hot::after]:opacity-100 \
[&::after]:[-webkit-mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)] \
[&::after]:[-webkit-mask-composite:xor] \
[&::after]:[mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)] \
[&::after]:[mask-composite:exclude]")
}),
);
wait_frame().await;
}
#[wasm_bindgen_test]
async fn child_and_state_variant_selectors_insert() {
let tc = TestContainer::new();
dominator::append_dom(
&tc.dom_element(),
html!("div", {
.dwclass!("[& > *]:opacity-0 [& > *]:[transform:translateY(26px)] \
[& > *]:[transition:opacity 650ms cubic-bezier(0.16, 1, 0.3, 1), transform 650ms cubic-bezier(0.16, 1, 0.3, 1)]")
.dwclass!("[& > *:nth-child(2)]:delay-75 [& > *:nth-child(5)]:[transition-delay:280ms]")
.dwclass!("[&.reveal-in > *]:opacity-100")
.dwclass!("[&:hover > *]:[animation-play-state:paused]")
}),
);
wait_frame().await;
}
#[wasm_bindgen_test]
async fn scrollbar_styling_uses_standard_properties_only() {
let tc = TestContainer::new();
dominator::append_dom(
&tc.dom_element(),
html!("div", {
.attr("id", "probe-scrollbar")
.dwclass!("[scrollbar-width:thin] [scrollbar-color:#26262C transparent]")
}),
);
wait_frame().await;
let doc = web_sys::window().unwrap().document().unwrap();
let el = doc.get_element_by_id("probe-scrollbar").unwrap();
let style = web_sys::window()
.unwrap()
.get_computed_style(&el)
.unwrap()
.unwrap();
assert_eq!(style.get_property_value("scrollbar-width").unwrap(), "thin");
}