diff --git a/crates/grida-canvas/src/html/mod.rs b/crates/grida-canvas/src/html/mod.rs index 6f2ac5557..cb851c964 100644 --- a/crates/grida-canvas/src/html/mod.rs +++ b/crates/grida-canvas/src/html/mod.rs @@ -1392,23 +1392,6 @@ mod tests { use crate::layout::ComputedLayout; use crate::node::schema::Scene; use std::collections::HashMap; - use std::sync::Mutex; - - /// Global mutex to serialize HTML tests. - /// - /// The CSS cascade adapter uses a process-global `DEMO_DOM` static, so - /// concurrent `from_html_str` calls race on that shared slot and cause - /// Stylo `debug_assert` panics ("Why are we here?"). A mutex ensures - /// only one test touches the global DOM at a time. - /// - /// We use `lock().unwrap_or_else(|e| e.into_inner())` to recover from - /// poison so that a single test failure doesn't cascade to all others. - static HTML_TEST_LOCK: Mutex<()> = Mutex::new(()); - - /// Lock the HTML test mutex, clearing poison if a prior test panicked. - fn lock_html() -> std::sync::MutexGuard<'static, ()> { - HTML_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner()) - } /// Parse HTML and run the layout engine, returning the scene and a /// map of every node's computed layout. @@ -1455,7 +1438,7 @@ mod tests { #[test] fn smoke_test_basic_html() { - let _guard = lock_html(); + let _guard = crate::stylo_test::lock(); let html = r#" @@ -1483,7 +1466,7 @@ mod tests { #[test] fn test_inline_style_attribute() { - let _guard = lock_html(); + let _guard = crate::stylo_test::lock(); let html = r#" @@ -1496,7 +1479,7 @@ mod tests { #[test] fn test_borders_and_shadows() { - let _guard = lock_html(); + let _guard = crate::stylo_test::lock(); let html = r#" @@ -1514,7 +1497,7 @@ mod tests { #[test] fn test_flex_alignment() { - let _guard = lock_html(); + let _guard = crate::stylo_test::lock(); let html = r#" @@ -1535,7 +1518,7 @@ mod tests { #[test] fn test_gradient_backgrounds() { - let _guard = lock_html(); + let _guard = crate::stylo_test::lock(); let html = r#" @@ -1560,7 +1543,7 @@ mod tests { /// 3 fixed-size divs in a flex row with gap. #[test] fn test_flex_row_positions() { - let _guard = lock_html(); + let _guard = crate::stylo_test::lock(); let html = r#"
@@ -1597,7 +1580,7 @@ mod tests { /// 3 fixed-size divs in a flex column with gap. #[test] fn test_flex_column_positions() { - let _guard = lock_html(); + let _guard = crate::stylo_test::lock(); let html = r#"
@@ -1630,7 +1613,7 @@ mod tests { /// justify-content: center with 2 fixed children. #[test] fn test_flex_justify_center() { - let _guard = lock_html(); + let _guard = crate::stylo_test::lock(); let html = r#"
@@ -1662,7 +1645,7 @@ mod tests { /// justify-content: space-between with 3 fixed children. #[test] fn test_flex_justify_space_between() { - let _guard = lock_html(); + let _guard = crate::stylo_test::lock(); let html = r#"
@@ -1694,7 +1677,7 @@ mod tests { /// align-items: center with a single child shorter than container. #[test] fn test_flex_align_center() { - let _guard = lock_html(); + let _guard = crate::stylo_test::lock(); let html = r#"
@@ -1722,7 +1705,7 @@ mod tests { /// flex-grow: second child fills remaining space. #[test] fn test_flex_grow() { - let _guard = lock_html(); + let _guard = crate::stylo_test::lock(); let html = r#"
@@ -1758,7 +1741,7 @@ mod tests { /// Container padding offsets children. #[test] fn test_flex_padding() { - let _guard = lock_html(); + let _guard = crate::stylo_test::lock(); let html = r#"
@@ -1787,7 +1770,7 @@ mod tests { /// Flex column gap direction is correct (gap applies vertically). #[test] fn test_flex_gap_column_direction() { - let _guard = lock_html(); + let _guard = crate::stylo_test::lock(); let html = r#"
@@ -1818,7 +1801,7 @@ mod tests { /// Nested flex: outer row, inner column with children. #[test] fn test_nested_flex() { - let _guard = lock_html(); + let _guard = crate::stylo_test::lock(); let html = r#"
@@ -1869,7 +1852,7 @@ mod tests { /// Explicit width/height dimensions are preserved. #[test] fn test_explicit_dimensions() { - let _guard = lock_html(); + let _guard = crate::stylo_test::lock(); let html = r#"
@@ -1896,7 +1879,7 @@ mod tests { /// flex-wrap: children that overflow wrap to the next line. #[test] fn test_flex_wrap() { - let _guard = lock_html(); + let _guard = crate::stylo_test::lock(); let html = r#"
@@ -1944,7 +1927,7 @@ mod tests { /// text-shadow maps to drop-shadow effects on the TextSpan node. #[test] fn test_text_shadow() { - let _guard = lock_html(); + let _guard = crate::stylo_test::lock(); let html = r#"
Hello
@@ -1982,7 +1965,7 @@ mod tests { /// Multiple text-shadows produce multiple DropShadow effects. #[test] fn test_text_shadow_multiple() { - let _guard = lock_html(); + let _guard = crate::stylo_test::lock(); let html = r#"
Multi
@@ -2009,7 +1992,7 @@ mod tests { /// box-shadow (inset + outer) maps to InnerShadow + DropShadow. #[test] fn test_box_shadow() { - let _guard = lock_html(); + let _guard = crate::stylo_test::lock(); let html = r#"
@@ -2044,7 +2027,7 @@ mod tests { /// filter: blur() maps to FeLayerBlur on a rectangle. #[test] fn test_filter_blur() { - let _guard = lock_html(); + let _guard = crate::stylo_test::lock(); let html = r#"
@@ -2075,7 +2058,7 @@ mod tests { /// filter: drop-shadow() maps to DropShadow effect on a rectangle. #[test] fn test_filter_drop_shadow() { - let _guard = lock_html(); + let _guard = crate::stylo_test::lock(); let html = r#"
@@ -2115,7 +2098,7 @@ mod tests { /// override is available. This test verifies the pipeline doesn't crash. #[test] fn test_backdrop_filter_blur() { - let _guard = lock_html(); + let _guard = crate::stylo_test::lock(); let html = r#"
@@ -2145,7 +2128,7 @@ mod tests { /// mix-blend-mode: multiply maps to LayerBlendMode::Blend(BlendMode::Multiply). #[test] fn test_mix_blend_mode() { - let _guard = lock_html(); + let _guard = crate::stylo_test::lock(); let html = r#"
@@ -2174,7 +2157,7 @@ mod tests { /// mix-blend-mode: normal stays as PassThrough (default). #[test] fn test_mix_blend_mode_normal() { - let _guard = lock_html(); + let _guard = crate::stylo_test::lock(); let html = r#"
@@ -2200,7 +2183,7 @@ mod tests { /// Effects on containers: filter + blend mode on a flex container. #[test] fn test_container_effects() { - let _guard = lock_html(); + let _guard = crate::stylo_test::lock(); let html = r#"
@@ -2241,7 +2224,7 @@ mod tests { /// text-decoration-color maps to TextDecorationRec.text_decoration_color. #[test] fn test_text_decoration_color() { - let _guard = lock_html(); + let _guard = crate::stylo_test::lock(); let html = r#"
Red underline
@@ -2280,7 +2263,7 @@ mod tests { /// text-decoration-style maps to TextDecorationRec.text_decoration_style. #[test] fn test_text_decoration_style() { - let _guard = lock_html(); + let _guard = crate::stylo_test::lock(); let html = r#"
Wavy
@@ -2315,7 +2298,7 @@ mod tests { /// Combined: text-decoration with color + style + line. #[test] fn test_text_decoration_combined() { - let _guard = lock_html(); + let _guard = crate::stylo_test::lock(); let html = r#"
Combo
@@ -2359,7 +2342,7 @@ mod tests { /// h1 margin should merge into padding (no extra wrapper container). #[test] fn test_h1_margin_no_double_wrap() { - let _guard = lock_html(); + let _guard = crate::stylo_test::lock(); let html = r#"

Hello

@@ -2409,7 +2392,7 @@ mod tests { /// Inline elements (, , ) should merge into a single AttributedText. #[test] fn test_inline_elements_merge_to_attributed_text() { - let _guard = lock_html(); + let _guard = crate::stylo_test::lock(); let html = r#"

Hello world!

@@ -2470,7 +2453,7 @@ mod tests { /// Whitespace between inline elements must be preserved. #[test] fn test_inline_whitespace_preserved() { - let _guard = lock_html(); + let _guard = crate::stylo_test::lock(); let html = r#"

Default red and green text.

diff --git a/crates/grida-canvas/src/htmlcss/mod.rs b/crates/grida-canvas/src/htmlcss/mod.rs index 08cf029f6..250b8d493 100644 --- a/crates/grida-canvas/src/htmlcss/mod.rs +++ b/crates/grida-canvas/src/htmlcss/mod.rs @@ -82,18 +82,13 @@ mod tests { use crate::resources::ByteStore; use std::sync::{Arc, Mutex}; - /// Stylo uses a process-global DOM slot that is not thread-safe. - /// All htmlcss tests must be serialized to avoid concurrent access. - /// We also share this with the `html` module's tests via crate-level visibility. - static TEST_LOCK: Mutex<()> = Mutex::new(()); - fn test_fonts() -> FontRepository { FontRepository::new(Arc::new(Mutex::new(ByteStore::new()))) } #[test] fn test_render_empty() { - let _guard = TEST_LOCK.lock().unwrap(); + let _guard = crate::stylo_test::lock(); let fonts = test_fonts(); let pic = render("", 400.0, 300.0, &fonts); assert!(pic.is_ok()); @@ -101,7 +96,7 @@ mod tests { #[test] fn test_render_heading() { - let _guard = TEST_LOCK.lock().unwrap(); + let _guard = crate::stylo_test::lock(); let fonts = test_fonts(); let pic = render("

Hello

", 400.0, 300.0, &fonts).unwrap(); assert!(pic.cull_rect().width() > 0.0); @@ -109,7 +104,7 @@ mod tests { #[test] fn test_render_with_style_block() { - let _guard = TEST_LOCK.lock().unwrap(); + let _guard = crate::stylo_test::lock(); let fonts = test_fonts(); let pic = render( "

Blue

", @@ -122,7 +117,7 @@ mod tests { #[test] fn test_render_table() { - let _guard = TEST_LOCK.lock().unwrap(); + let _guard = crate::stylo_test::lock(); let fonts = test_fonts(); let pic = render( "
AB
", @@ -135,7 +130,7 @@ mod tests { #[test] fn test_render_flex() { - let _guard = TEST_LOCK.lock().unwrap(); + let _guard = crate::stylo_test::lock(); let fonts = test_fonts(); let pic = render( r#"
A
B
"#, @@ -149,7 +144,7 @@ mod tests { /// Verify grid properties are collected and layout produces columns. #[test] fn test_grid_layout_columns() { - let _guard = TEST_LOCK.lock().unwrap(); + let _guard = crate::stylo_test::lock(); let fonts = test_fonts(); let html = r#"
@@ -222,7 +217,7 @@ mod tests { #[test] fn test_render_grid_basic() { - let _guard = TEST_LOCK.lock().unwrap(); + let _guard = crate::stylo_test::lock(); let fonts = test_fonts(); let pic = render( r#"
@@ -237,7 +232,7 @@ mod tests { #[test] fn test_render_grid_fr() { - let _guard = TEST_LOCK.lock().unwrap(); + let _guard = crate::stylo_test::lock(); let fonts = test_fonts(); let pic = render( r#"
@@ -252,7 +247,7 @@ mod tests { #[test] fn test_render_grid_repeat() { - let _guard = TEST_LOCK.lock().unwrap(); + let _guard = crate::stylo_test::lock(); let fonts = test_fonts(); let pic = render( r#"
@@ -267,7 +262,7 @@ mod tests { #[test] fn test_render_grid_span() { - let _guard = TEST_LOCK.lock().unwrap(); + let _guard = crate::stylo_test::lock(); let fonts = test_fonts(); let pic = render( r#"
@@ -284,7 +279,7 @@ mod tests { #[test] fn test_render_grid_auto_flow_dense() { - let _guard = TEST_LOCK.lock().unwrap(); + let _guard = crate::stylo_test::lock(); let fonts = test_fonts(); let pic = render( r#"
@@ -302,7 +297,7 @@ mod tests { #[test] fn test_render_box_shadow_outer() { - let _guard = TEST_LOCK.lock().unwrap(); + let _guard = crate::stylo_test::lock(); let fonts = test_fonts(); let pic = render( r#"
shadow
"#, @@ -315,7 +310,7 @@ mod tests { #[test] fn test_render_box_shadow_inset() { - let _guard = TEST_LOCK.lock().unwrap(); + let _guard = crate::stylo_test::lock(); let fonts = test_fonts(); let pic = render( r#"
inset
"#, @@ -328,7 +323,7 @@ mod tests { #[test] fn test_render_box_shadow_combined() { - let _guard = TEST_LOCK.lock().unwrap(); + let _guard = crate::stylo_test::lock(); let fonts = test_fonts(); let pic = render( r#"
both
"#, @@ -342,7 +337,7 @@ mod tests { /// Verify box-shadow properties are collected from Stylo. #[test] fn test_box_shadow_collection() { - let _guard = TEST_LOCK.lock().unwrap(); + let _guard = crate::stylo_test::lock(); let html = r#"
shadow
"#; let root = collect::collect_styled_tree(html).unwrap().unwrap(); @@ -372,7 +367,7 @@ mod tests { #[test] fn test_render_opacity() { - let _guard = TEST_LOCK.lock().unwrap(); + let _guard = crate::stylo_test::lock(); let fonts = test_fonts(); let pic = render( r#"

Semi-transparent

"#, @@ -442,7 +437,7 @@ mod tests { #[test] fn test_measure_height() { - let _guard = TEST_LOCK.lock().unwrap(); + let _guard = crate::stylo_test::lock(); let fonts = test_fonts(); let h = measure_content_height("

Hello

", 400.0, &fonts).unwrap(); assert!(h > 0.0, "Content height should be positive, got {h}"); @@ -450,7 +445,7 @@ mod tests { #[test] fn test_head_hidden() { - let _guard = TEST_LOCK.lock().unwrap(); + let _guard = crate::stylo_test::lock(); let fonts = test_fonts(); let pic = render( r#"

V

"#, @@ -467,7 +462,7 @@ mod tests { #[test] fn test_markdown_heading() { - let _guard = TEST_LOCK.lock().unwrap(); + let _guard = crate::stylo_test::lock(); let fonts = test_fonts(); let html = markdown_to_styled_html("# Hello World"); let pic = render(&html, 400.0, 300.0, &fonts); @@ -477,7 +472,7 @@ mod tests { #[test] fn test_markdown_mixed_content() { - let _guard = TEST_LOCK.lock().unwrap(); + let _guard = crate::stylo_test::lock(); let fonts = test_fonts(); let md = r#"# Title @@ -509,7 +504,7 @@ code block #[test] fn test_markdown_table() { - let _guard = TEST_LOCK.lock().unwrap(); + let _guard = crate::stylo_test::lock(); let fonts = test_fonts(); let md = r#" | Name | Age | City | @@ -524,7 +519,7 @@ code block #[test] fn test_markdown_empty() { - let _guard = TEST_LOCK.lock().unwrap(); + let _guard = crate::stylo_test::lock(); let fonts = test_fonts(); let html = markdown_to_styled_html(""); let pic = render(&html, 400.0, 300.0, &fonts); diff --git a/crates/grida-canvas/src/lib.rs b/crates/grida-canvas/src/lib.rs index bf483db08..ee5e18497 100644 --- a/crates/grida-canvas/src/lib.rs +++ b/crates/grida-canvas/src/lib.rs @@ -27,3 +27,20 @@ pub mod text_edit; pub mod text_edit_session; pub mod vectornetwork; pub mod window; + +/// Shared test lock for Stylo's process-global DOM slot. +/// +/// Both `html` and `htmlcss` modules use Stylo which is **not** thread-safe. +/// All tests that call into Stylo must hold this lock. We use +/// `unwrap_or_else(|e| e.into_inner())` to recover from poison so a single +/// test panic does not cascade to every other Stylo test. +#[cfg(test)] +pub(crate) mod stylo_test { + use std::sync::{Mutex, MutexGuard}; + + static STYLO_TEST_LOCK: Mutex<()> = Mutex::new(()); + + pub fn lock() -> MutexGuard<'static, ()> { + STYLO_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner()) + } +}