From 9c59eb21eb4facfef2220380569dff04fb75344f Mon Sep 17 00:00:00 2001 From: Thomas Gazagnaire Date: Sun, 13 Sep 2026 18:29:51 +0200 Subject: [PATCH] test: hold masks, svg, effects and prose to whole declarations A mask utility aliases its property for WebKit and every affix here was satisfied by the unaliased arm alone. In prose the affix ".prose" matched all 77 rules the component writes, root and descendants alike, and the h1 one stopped mid-selector. Three effects checks searched for a spelling that must not appear, which a sheet with no rule in it also satisfies. --- test/test_effects.ml | 79 +++++++++++++++-------------------- test/test_masks.ml | 98 +++++++++++++++----------------------------- test/test_prose.ml | 53 +++++++++++++----------- test/test_svg.ml | 46 ++++----------------- 4 files changed, 105 insertions(+), 171 deletions(-) diff --git a/test/test_effects.ml b/test/test_effects.ml index c8b8e49f0..c917ebd44 100644 --- a/test/test_effects.ml +++ b/test/test_effects.ml @@ -256,17 +256,15 @@ let test_inset_shadow_theme_override () = Tw.Scheme.with_overrides Tw.Scheme.default [ ("inset-shadow-sm", "inset 0 1px 1px rgb(0 0 0 / 0.05)") ] in - let css = - Tw.to_css ~theme ~base:false - [ Result.get_ok (Tw.of_string ~theme "inset-shadow-sm") ] - |> Tw.Css.to_string ~minify:true - in - Alcotest.(check bool) - "inset-shadow-sm @theme override flows to [inset 0 1px 1px]" true - (Astring.String.is_infix ~affix:"inset 0 1px 1px" css); - Alcotest.(check bool) - "inset-shadow-sm @theme override drops the default [inset 0 2px 4px]" false - (Astring.String.is_infix ~affix:"inset 0 2px 4px" css) + (* The whole list, which is where "drops the default" is said: it used to be a + second search for a spelling that must not appear, and a sheet with no rule + in it satisfies that too. *) + Test_helpers.check_declarations ~theme "inset-shadow-sm" + [ + "--tw-inset-shadow:inset 0 1px 1px var(--tw-inset-shadow-color,rgb(0 0 \ + 0/.05))"; + composes_box_shadow; + ] (* The override is read through the CSS shadow grammar, so it carries whatever that grammar allows: a length in any unit, a fourth length for the spread, @@ -275,28 +273,26 @@ let test_inset_shadow_theme_override () = the rest, which put the built-in [inset 0 2px 4px] in the sheet and lost the override without saying so. *) let test_inset_shadow_theme_override_grammar () = - let sheet override = + (* The whole list per override. "The default is not substituted" was a search + for a spelling that must not appear; the list says it by construction, and + also that the colour the project wrote reached the fallback slot. *) + let holds ~override body = let theme = Tw.Scheme.with_overrides Tw.Scheme.default [ ("inset-shadow-sm", override) ] in - Tw.to_css ~theme ~base:false - [ Result.get_ok (Tw.of_string ~theme "inset-shadow-sm") ] - |> Tw.Css.to_string ~minify:true - in - let holds ~name ~override affix = - Alcotest.(check bool) - name true - (Astring.String.is_infix ~affix (sheet override)) + Test_helpers.check_declarations ~theme "inset-shadow-sm" + [ + "--tw-inset-shadow:" ^ body + ^ " var(--tw-inset-shadow-color,rgb(0 0 0/.05))"; + composes_box_shadow; + ] in - holds ~name:"an em override keeps its unit" - ~override:"inset 0 0.125em 0.25em rgb(0 0 0 / 0.05)" "inset 0 .125em .25em"; - holds ~name:"a fourth length is the spread" - ~override:"inset 0 1px 2px 3px rgb(0 0 0 / 0.05)" "inset 0 1px 2px 3px"; - Alcotest.(check bool) - "the default is not substituted" false - (Astring.String.is_infix ~affix:"inset 0 2px 4px" - (sheet "inset 0 0.125em 0.25em rgb(0 0 0 / 0.05)")) + (* an em override keeps its unit *) + holds ~override:"inset 0 0.125em 0.25em rgb(0 0 0 / 0.05)" + "inset 0 .125em .25em"; + (* a fourth length is the spread *) + holds ~override:"inset 0 1px 2px 3px rgb(0 0 0 / 0.05)" "inset 0 1px 2px 3px" (* A shadeless colour has no shade segment, so shadow-white never reached the colour parse: the size cases claimed the segment and rejected it. The class @@ -609,14 +605,9 @@ let test_bracket_hex_opacity_var () = comes from a separate, correctly-scaled computation, so --tw-shadow-alpha here is a plain, unconverted echo of what the class wrote. *) let test_shadow_bracket_alpha_tracking () = - let css cls = - match Tw.of_string cls with - | Ok u -> Tw.to_css ~base:false [ u ] |> Tw.Css.to_string ~minify:true - | Error (`Msg m) -> Alcotest.failf "%s: %s" cls m - in - let lacks cls affix = - Alcotest.(check bool) cls false (Astring.String.is_infix ~affix (css cls)) - in + (* The [lacks] checks that used to sit beside each list here - "no + --tw-shadow-alpha:2500%" - said nothing the list does not already say, and + passed on a sheet with no rule in it. *) let shadow_lg alpha painted = [ "--tw-shadow-alpha:" ^ alpha; @@ -627,7 +618,6 @@ let test_shadow_bracket_alpha_tracking () = in Test_helpers.check_declarations "shadow-lg/[25]" (shadow_lg "25" "oklab(0%0 0/25)"); - lacks "shadow-lg/[25]" "--tw-shadow-alpha:2500%"; Test_helpers.check_declarations "shadow-[0_1px_2px_#000]/[25]" [ "--tw-shadow-alpha:25"; @@ -641,7 +631,6 @@ let test_shadow_bracket_alpha_tracking () = 0/25))"; composes_box_shadow; ]; - lacks "inset-shadow-sm/[25]" "--tw-inset-shadow-alpha:2500%"; (* A bracket alpha that does carry a [%] sign, or the plain percent form, both keep behaving as a percentage. *) Test_helpers.check_declarations "shadow-lg/[25%]" @@ -784,13 +773,13 @@ let test_project_shadow_tokens () = ("inset-shadow-dent", "inset 0 1px 2px #000"); ] in - let css cls = - match Tw.of_string ~theme cls with - | Ok u -> Tw.to_css ~theme ~base:false [ u ] |> Tw.Css.to_string - | Error (`Msg m) -> Alcotest.failf "%s: %s" cls m - in - let emits affix cls = - Alcotest.(check bool) cls true (Astring.String.is_infix ~affix (css cls)) + let emits decl cls = + Test_helpers.check_declarations ~theme ~minify:false cls + [ + decl; + "box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), \ + var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)"; + ] in emits "--tw-shadow: 0 0 8px var(--tw-shadow-color, #f00)" "shadow-halo"; emits "--tw-inset-shadow: inset 0 1px 2px var(--tw-inset-shadow-color, #000)" diff --git a/test/test_masks.ml b/test/test_masks.ml index 69635b983..505ca22cd 100644 --- a/test/test_masks.ml +++ b/test/test_masks.ml @@ -47,15 +47,13 @@ let test_typed () = (* [mask-[]] takes any background-image, not only a linear-gradient. *) let test_bracket_image () = - let css cls = - match Tw.of_string cls with - | Ok u -> Tw.to_css ~base:false [ u ] |> Tw.Css.to_string ~minify:true - | Error (`Msg m) -> Alcotest.failf "%s: %s" cls m - in - Alcotest.(check bool) - "radial-gradient reaches mask-image" true - (Astring.String.is_infix ~affix:"mask-image:radial-gradient(white,black)" - (css "mask-[radial-gradient(white,black)]")); + (* The whole list. A mask utility aliases its property for WebKit, so the + affix this replaces was satisfied by the unaliased arm alone. *) + Test_helpers.check_declarations "mask-[radial-gradient(white,black)]" + [ + "-webkit-mask-image:radial-gradient(white,black)"; + "mask-image:radial-gradient(white,black)"; + ]; check "mask-[radial-gradient(white,black)]"; check "mask-[conic-gradient(white,black)]"; check "mask-[linear-gradient(white,black)]" @@ -69,19 +67,12 @@ let test_bracket_layer_list () = [30% 50%] compacts to [30%50%] - still two components, still valid, and it round-trips - while a different minifier folds the pair to its [x] alone. Pinning either spelling pins the minifier instead of the reading. *) - let css cls = - match Tw.of_string cls with - | Ok u -> Tw.to_css ~base:false [ u ] |> Tw.Css.to_string - | Error (`Msg m) -> Alcotest.failf "%s: %s" cls m + let both cls prop value = + Test_helpers.check_declarations ~minify:false cls + [ "-webkit-" ^ prop ^ ": " ^ value; prop ^ ": " ^ value ] in - Alcotest.(check bool) - "two url layers stay two" true - (Astring.String.is_infix ~affix:"mask-image: url(/a.png), url(/b.png)" - (css "mask-[url(/a.png),url(/b.png)]")); - Alcotest.(check bool) - "two positions stay two" true - (Astring.String.is_infix ~affix:"mask-position: 30% 50%, 70% 50%" - (css "mask-position-[30%_50%,70%_50%]")) + both "mask-[url(/a.png),url(/b.png)]" "mask-image" "url(/a.png), url(/b.png)"; + both "mask-position-[30%_50%,70%_50%]" "mask-position" "30% 50%, 70% 50%" (* A bracket that would end the declaration or swallow what follows it is the one thing no mask utility can hold, and Tailwind writes nothing for it @@ -168,17 +159,13 @@ let test_bracket_falls_through_to_a_longhand () = rem before; every CSS length unit does now, matching real Tailwind's mask-size-[2em]. *) let test_bracket_size_units () = - let css cls = - match Tw.of_string cls with - | Ok u -> Tw.to_css ~base:false [ u ] |> Tw.Css.to_string ~minify:true - | Error (`Msg m) -> Alcotest.failf "%s: %s" cls m - in - let has cls affix = - Alcotest.(check bool) cls true (Astring.String.is_infix ~affix (css cls)) + let has cls = + Test_helpers.check_declarations cls + [ "-webkit-mask-size:2em"; "mask-size:2em" ] in - has "mask-size-[2em]" "mask-size:2em"; - has "mask-[size:2em]" "mask-size:2em"; - has "mask-[length:2em]" "mask-size:2em" + has "mask-size-[2em]"; + has "mask-[size:2em]"; + has "mask-[length:2em]" (* A mask-position bracket takes the whole CSS grammar, the same as background-position: a single edge keyword and the edge/offset form. Both @@ -187,20 +174,16 @@ let test_bracket_position_grammar () = (* Unminified, so the assertions read as the grammar they are about. Compact spelling belongs to the minifier: [10px 20px] keeps its space while [30% 50%] loses it, because a [%] already ends the token. *) - let css cls = - match Tw.of_string cls with - | Ok u -> Tw.to_css ~base:false [ u ] |> Tw.Css.to_string - | Error (`Msg m) -> Alcotest.failf "%s: %s" cls m - in - let has cls affix = - Alcotest.(check bool) cls true (Astring.String.is_infix ~affix (css cls)) + let has cls value = + Test_helpers.check_declarations ~minify:false cls + [ "-webkit-mask-position: " ^ value; "mask-position: " ^ value ] in - has "mask-[position:top]" "mask-position: top"; - has "mask-position-[top]" "mask-position: top"; - has "mask-[top]" "mask-position: top"; + has "mask-[position:top]" "top"; + has "mask-position-[top]" "top"; + has "mask-[top]" "top"; (* the lengths and layer-list forms are unchanged *) - has "mask-[position:10px_20px]" "mask-position: 10px 20px"; - has "mask-position-[30%_50%,70%_50%]" "mask-position: 30% 50%, 70% 50%" + has "mask-[position:10px_20px]" "10px 20px"; + has "mask-position-[30%_50%,70%_50%]" "30% 50%, 70% 50%" (* Masks sit between the backgrounds and fill/stroke, and the mask-gradient utilities lead them. Sharing padding's slot interleaved the two families with @@ -228,15 +211,8 @@ let order_matches_tailwind () = (* A mask image is an arbitrary value, so [_] is a space and [\_] a literal underscore: a file name carrying one is written with the escape. *) let test_bracket_image_underscore_escape () = - let css cls = - match Tw.of_string cls with - | Ok u -> Tw.to_css ~base:false [ u ] |> Tw.Css.to_string - | Error (`Msg m) -> Alcotest.failf "%s: %s" cls m - in - Alcotest.(check bool) - "an escaped underscore stays in the file name" true - (Astring.String.is_infix ~affix:"mask-image: url('a_b.png')" - (css {|mask-[url('a\_b.png')]|})) + Test_helpers.check_declarations ~minify:false {|mask-[url('a\_b.png')]|} + [ "-webkit-mask-image: url('a_b.png')"; "mask-image: url('a_b.png')" ] (* Tailwind leaves a bare [_] alone inside [url()], where it is part of the file name rather than an encoded space, so [mask-[url('a_b.png')]] names @@ -252,22 +228,14 @@ let test_bracket_url_keeps_underscores () = name too. Only the [_] outside the url is a space, which [image-set()] is the case that tells the two apart. *) let test_bracket_url_underscore () = - let css cls = - match Tw.of_string cls with - | Ok u -> Tw.to_css ~base:false [ u ] |> Tw.Css.to_string - | Error (`Msg m) -> Alcotest.failf "%s: %s" cls m - in - let has cls affix = - Alcotest.(check bool) - (cls ^ " emits " ^ affix) - true - (Astring.String.is_infix ~affix (css cls)) + let has cls value = + Test_helpers.check_declarations ~minify:false cls + [ "-webkit-mask-image: " ^ value; "mask-image: " ^ value ] in - has "mask-[url('a_b.png')]" "mask-image: url('a_b.png')"; + has "mask-[url('a_b.png')]" "url('a_b.png')"; (* Tailwind writes the inner url quoted. The quoting is cascade's canonical spelling of the same URL; the underscore is the point. *) - has "mask-[image-set(url('a_b.png')_1x)]" - "mask-image: image-set(url(a_b.png) 1x)" + has "mask-[image-set(url('a_b.png')_1x)]" "image-set(url(a_b.png) 1x)" (* A [url()] carrying a position is no [url()] token, so the typed reading has no answer for it and the whole bracket goes out verbatim instead. What must diff --git a/test/test_prose.ml b/test/test_prose.ml index 8550bcd8a..11b7507d5 100644 --- a/test/test_prose.ml +++ b/test/test_prose.ml @@ -23,26 +23,29 @@ let test_combinations () = Alcotest.(check string) "complex prose" "prose prose-xl prose-gray mx-auto" complex +(* [prose] is a component, so its 77 rules are not worth spelling out; what + these check is that the right selectors and the right declarations are among + them. Membership in the lists is exact where a substring was not: the affix + [.prose] matched every descendant rule as well as the root one, and the h1 + affix stopped mid-selector. *) let test_css_generation () = - (* Test that prose generates CSS rules *) - let css = to_css [ prose ] in - let css_string = Css.to_string css in - - (* Check that CSS variables are included *) + let utility = + match Tw.of_string "prose" with + | Ok u -> u + | Error (`Msg m) -> Alcotest.failf "prose: %s" m + in + let selectors = Test_helpers.selectors_of_utility utility in + let has_selector sel = + Alcotest.(check bool) ("has " ^ sel) true (List.mem sel selectors) + in + has_selector ".prose"; + has_selector + {|.prose :where(h1):not(:where([class~="not-prose"], [class~="not-prose"] *))|}; + (* The root rule reads the body variable rather than inlining a colour. *) Alcotest.(check bool) "has prose body variable" true - (Astring.String.is_infix ~affix:"--tw-prose-body" css_string); - - (* Check that prose class is generated *) - Alcotest.(check bool) - "has prose class" true - (Astring.String.is_infix ~affix:".prose" css_string); - - (* Check that descendant selectors are generated *) - Alcotest.(check bool) - "has prose h1 selector" true - (Astring.String.is_infix - ~affix:".prose :where(h1):not(:where([class~=\"not-prose\"]" css_string) + (List.mem "color:var(--tw-prose-body)" + (Test_helpers.declarations_of_class "prose")) let test_inline_styles () = (* Prose utilities can generate inline styles from their rules, but CSS @@ -63,15 +66,17 @@ let test_inline_styles () = (* prose-invert remaps the palette to the inverted vars; prose-orange overrides the link accent colours. Both used to be no-ops / unknown. *) let test_color_variants () = - let invert = Css.to_string (to_css ~base:false [ prose_invert ]) in Alcotest.(check bool) "prose-invert remaps body to the invert var" true - (Astring.String.is_infix ~affix:"var(--tw-prose-invert-body)" invert); - let orange = Css.to_string (to_css ~base:false [ prose_orange ]) in - Alcotest.(check bool) - "prose-orange sets the link accent" true - (Astring.String.is_infix ~affix:"--tw-prose-links" orange - && Astring.String.is_infix ~affix:"--tw-prose-invert-links" orange) + (List.mem "--tw-prose-body:var(--tw-prose-invert-body)" + (Test_helpers.declarations_of_class "prose-invert")); + (* A colour variant sets only the accents it names, which the pair of searches + this replaces could not say. *) + Test_helpers.check_declarations "prose-orange" + [ + "--tw-prose-links:oklch(64.6%.222 41.116)"; + "--tw-prose-invert-links:oklch(70.5%.213 47.604)"; + ] (* The colours [prose] sets on its own are the gray palette, normal and inverted. Nothing else in the suite reads them, so an edit to [gray_normal] diff --git a/test/test_svg.ml b/test/test_svg.ml index 62ef8c6fb..f9b2cbede 100644 --- a/test/test_svg.ml +++ b/test/test_svg.ml @@ -10,25 +10,15 @@ let basic_svg () = stroke-width case and rejected; the width case now only matches integers, so they reach the colour parse. *) let stroke_shadeless_colors () = - let css cls = - match Tw.of_string cls with - | Ok u -> Tw.to_css ~base:false [ u ] |> Tw.Css.to_string ~minify:true - | Error (`Msg m) -> Alcotest.failf "%s: %s" cls m - in - Alcotest.(check bool) - "stroke-white references --color-white" true - (Astring.String.is_infix ~affix:"stroke:var(--color-white)" - (css "stroke-white")); - Alcotest.(check bool) - "stroke-black references --color-black" true - (Astring.String.is_infix ~affix:"stroke:var(--color-black)" - (css "stroke-black")); + (* The whole list, which is what "parses as a colour, not a width" means: the + affix on [stroke:] left open whether a [stroke-width] was written beside + it. *) + Test_helpers.check_declarations "stroke-white" [ "stroke:var(--color-white)" ]; + Test_helpers.check_declarations "stroke-black" [ "stroke:var(--color-black)" ]; (* Integer widths still parse as widths, not colours. The [px] is Tailwind's minifier reading its generator's bare [2], and it is the spelling the upstream corpus records and [--diff] compares. *) - Alcotest.(check bool) - "stroke-2 stays a width" true - (Astring.String.is_infix ~affix:"stroke-width:2px" (css "stroke-2")) + Test_helpers.check_declarations "stroke-2" [ "stroke-width:2px" ] let stroke_light_dark_color () = let cls = "stroke-[light-dark(red,blue)]" in @@ -36,24 +26,13 @@ let stroke_light_dark_color () = | Error (`Msg m) -> Alcotest.failf "%s: %s" cls m | Ok u -> Alcotest.(check string) "class" cls (Tw.pp u); - let css = Tw.to_css ~base:false [ u ] |> Tw.Css.to_string ~minify:true in - Alcotest.(check bool) - "light-dark() is routed as a stroke colour" true - (Astring.String.is_infix ~affix:"stroke:light-dark(red,blue)" css) + Test_helpers.check_declarations cls [ "stroke:light-dark(red,blue)" ] (* An arbitrary stroke width is read with the whole CSS length grammar, so a unit the reader does not name is not silently rendered as a zero width. *) let stroke_arbitrary_width_units () = - let width cls = - match Tw.of_string cls with - | Ok u -> Tw.to_css ~base:false [ u ] |> Tw.Css.to_string ~minify:true - | Error (`Msg m) -> Alcotest.failf "%s: %s" cls m - in let emits cls value = - Alcotest.(check bool) - (cls ^ " emits " ^ value) - true - (Astring.String.is_infix ~affix:("stroke-width:" ^ value) (width cls)) + Test_helpers.check_declarations cls [ "stroke-width:" ^ value ] in emits "stroke-[1.5rem]" "1.5rem"; emits "stroke-[2em]" "2em"; @@ -105,14 +84,7 @@ let stroke_width_rejects_ocaml_literals () = stroke read the bracket text back as a hex and answered black for every colour with no hex spelling. *) let bracket_named_color () = - let css cls = - match Tw.of_string cls with - | Ok u -> Tw.to_css ~base:false [ u ] |> Tw.Css.to_string ~minify:true - | Error (`Msg m) -> Alcotest.failf "%s: %s" cls m - in - let emits affix cls = - Alcotest.(check bool) cls true (Astring.String.is_infix ~affix (css cls)) - in + let emits decl cls = Test_helpers.check_declarations cls [ decl ] in emits "stroke:rebeccapurple" "stroke-[rebeccapurple]"; emits "stroke:currentColor" "stroke-[currentColor]"; (* the modifier mixes into the colour the bracket named, not into black *)