From bb01aad5d63ee287e62563e5378243b5c4774964 Mon Sep 17 00:00:00 2001 From: Brett Bonner Date: Wed, 25 Feb 2026 20:56:02 -0800 Subject: [PATCH 1/3] Strengthen low-signal component tests with behavioral assertions --- docs/test_audit_headscratchers.md | 63 ++++++++++++ test/carousel_test.gleam | 164 +++++++++++++++--------------- test/direction_test.gleam | 24 ++++- test/form_test.gleam | 19 ++-- test/input_otp_test.gleam | 14 ++- test/item_test.gleam | 36 +++---- test/menubar_test.gleam | 117 ++++++--------------- test/navigation_menu_test.gleam | 60 +++++++---- test/resizable_test.gleam | 152 ++++++++++----------------- test/textarea_test.gleam | 14 +++ 10 files changed, 335 insertions(+), 328 deletions(-) create mode 100644 docs/test_audit_headscratchers.md diff --git a/docs/test_audit_headscratchers.md b/docs/test_audit_headscratchers.md new file mode 100644 index 0000000..f76c636 --- /dev/null +++ b/docs/test_audit_headscratchers.md @@ -0,0 +1,63 @@ +# Test Audit: Potentially "Fishy" / Low-Signal Tests + +This pass focuses on tests that look like they may have been written to keep green status rather than to strongly verify behavior. + +## Heuristics used + +- **Self-fulfilling round-trips**: setter + getter + predicate all from same module, no independent oracle. +- **Wrapper-equals-wrapper tests**: compares façade output to canonical helper output but never validates user-observable behavior. +- **Weak markup checks**: only checks `string.contains` for a broad token (`data-slot=...`) that may pass even if behavior regresses. +- **"Function exists" checks**: assertions like `helper() != []` that only prove non-empty output, not correctness. + +## Head-scratcher list + +1. `test/navigation_menu_test.gleam` + - `trigger style helper remains available` (headless + styled) only checks non-empty style list (`... != []`), which is weak and likely to pass through regressions. + - `viewport-enabled flag round-trips through config helpers` (headless + styled) is setter/getter self-validation with no render/event consequence check. + - Rendering tests only assert a few `data-slot` markers and do not verify viewport toggling behavior when disabled. + +2. `test/carousel_test.gleam` + - `orientation helpers round-trip...` (headless + styled) uses module-local setter/getter/predicate chain; high risk of tautological pass. + - Rendering assertions only check 3 slot markers (`carousel`, `carousel-item`, `carousel-next`), missing prev state/disabled semantics/orientation-specific rendering effects. + +3. `test/resizable_test.gleam` + - Orientation and handle tests are mostly helper round-trips with little externally validated behavior. + - Render test expects `aria-orientation="vertical"` but does not explicitly set orientation in test setup, coupling to defaults and making intent brittle. + +4. `test/item_test.gleam` + - Styled mutator tests are round-trips through same API (`item_variant` -> `item_config_variant` -> `item_variant_is_*`) with no independent expected value check. + - Render tests only verify slot-marker presence and not semantic behavior. + +5. `test/menubar_test.gleam` + - `root config key mutators keep menubar renderable` (headless + styled) checks only that rendering still includes `data-slot="menubar"`; this can pass even if callbacks are ignored. + - Item variant/inset tests are helper round-trips with no UI consequence assertions. + +6. `test/form_test.gleam` + - Façade-forwarding tests compare rendered strings from two related APIs; useful for aliasing checks but weak for product behavior. + - `styled form root renders semantic form container` only checks ` headless_carousel.carousel_orientation(orientation: vertical) - let content_config = - headless_carousel.carousel_content_config() - |> headless_carousel.carousel_content_orientation( - orientation: vertical, - ) - let item_config = - headless_carousel.carousel_item_config() - |> headless_carousel.carousel_item_orientation( - orientation: vertical, - ) - - headless_carousel.carousel_orientation_is_vertical( - orientation: headless_carousel.carousel_config_orientation( - config: root_config, - ), - ) - |> expect.to_equal(expected: True) + describe("headless behavior", [ + it("vertical orientation is reflected on root/content/item", fn() { + let vertical = headless_carousel.carousel_vertical() - headless_carousel.carousel_orientation_is_vertical( - orientation: headless_carousel.carousel_content_config_orientation( - config: content_config, - ), + let view = + headless_carousel.carousel( + config: headless_carousel.carousel_config() + |> headless_carousel.carousel_orientation(orientation: vertical), + children: [ + headless_carousel.carousel_content( + config: headless_carousel.carousel_content_config() + |> headless_carousel.carousel_content_orientation( + orientation: vertical, + ), + children: [ + headless_carousel.carousel_item( + config: headless_carousel.carousel_item_config() + |> headless_carousel.carousel_item_orientation( + orientation: vertical, + ), + child: weft_lustre.text(content: "Slide 1"), + ), + ], + ), + ], ) - |> expect.to_equal(expected: True) - headless_carousel.carousel_orientation_is_vertical( - orientation: headless_carousel.carousel_item_config_orientation( - config: item_config, - ), + let rendered = + weft_lustre.layout(attrs: [], child: view) + |> element.to_string + + string.contains(rendered, "data-orientation=\"vertical\"") + |> expect.to_equal(expected: True) + }), + it("disabled control renders disabled attribute", fn() { + let rendered = + headless_carousel.carousel_previous( + config: headless_carousel.carousel_control_config() + |> headless_carousel.carousel_control_disabled(), ) - |> expect.to_equal(expected: True) - }, - ), + |> weft_lustre.layout(attrs: []) + |> element.to_string + + string.contains(rendered, "disabled") + |> expect.to_equal(expected: True) + }), ]), describe("headless rendering", [ it("renders carousel root/content/item/control slots", fn() { @@ -90,57 +94,49 @@ pub fn carousel_tests() { |> expect.to_equal(expected: True) }), ]), - describe("styled config mutators", [ - it( - "orientation helpers round-trip across root/content/item configs", - fn() { - let t = theme.theme_default() - let vertical = ui_carousel.carousel_vertical(theme: t) - - let root_config = - ui_carousel.carousel_config(theme: t) - |> ui_carousel.carousel_orientation(theme: t, orientation: vertical) - let content_config = - ui_carousel.carousel_content_config(theme: t) - |> ui_carousel.carousel_content_orientation( - theme: t, - orientation: vertical, - ) - let item_config = - ui_carousel.carousel_item_config(theme: t) - |> ui_carousel.carousel_item_orientation( - theme: t, - orientation: vertical, - ) + describe("styled behavior", [ + it("styled vertical orientation is reflected in rendered markup", fn() { + let t = theme.theme_default() + let vertical = ui_carousel.carousel_vertical(theme: t) - ui_carousel.carousel_orientation_is_vertical( + let view = + ui_carousel.carousel( theme: t, - orientation: ui_carousel.carousel_config_orientation( - theme: t, - config: root_config, - ), + config: ui_carousel.carousel_config(theme: t) + |> ui_carousel.carousel_orientation( + theme: t, + orientation: vertical, + ), + children: [ + ui_carousel.carousel_content( + theme: t, + config: ui_carousel.carousel_content_config(theme: t) + |> ui_carousel.carousel_content_orientation( + theme: t, + orientation: vertical, + ), + children: [ + ui_carousel.carousel_item( + theme: t, + config: ui_carousel.carousel_item_config(theme: t) + |> ui_carousel.carousel_item_orientation( + theme: t, + orientation: vertical, + ), + child: weft_lustre.text(content: "Slide 1"), + ), + ], + ), + ], ) - |> expect.to_equal(expected: True) - ui_carousel.carousel_orientation_is_vertical( - theme: t, - orientation: ui_carousel.carousel_content_config_orientation( - theme: t, - config: content_config, - ), - ) - |> expect.to_equal(expected: True) + let rendered = + weft_lustre.layout(attrs: [], child: view) + |> element.to_string - ui_carousel.carousel_orientation_is_vertical( - theme: t, - orientation: ui_carousel.carousel_item_config_orientation( - theme: t, - config: item_config, - ), - ) - |> expect.to_equal(expected: True) - }, - ), + string.contains(rendered, "data-orientation=\"vertical\"") + |> expect.to_equal(expected: True) + }), ]), describe("styled rendering", [ it("renders carousel root/content/item/control slots", fn() { diff --git a/test/direction_test.gleam b/test/direction_test.gleam index af1a8ce..5851c3c 100644 --- a/test/direction_test.gleam +++ b/test/direction_test.gleam @@ -10,7 +10,7 @@ import weft_lustre_ui/theme pub fn direction_tests() { describe("direction", [ describe("headless direction", [ - it("direction_provider applies the configured dir attribute", fn() { + it("direction_provider applies rtl dir attribute", fn() { let config = headless_direction.direction_provider_config( direction: headless_direction.direction_rtl(), @@ -25,10 +25,27 @@ pub fn direction_tests() { string.contains(rendered, "dir=\"rtl\"") |> expect.to_equal(expected: True) + + string.contains(rendered, "dir=\"ltr\"") + |> expect.to_equal(expected: False) + }), + it("direction_provider applies ltr dir attribute", fn() { + let rendered = + headless_direction.direction_provider( + config: headless_direction.direction_provider_config( + direction: headless_direction.direction_ltr(), + ), + children: [], + ) + |> weft_lustre.layout(attrs: []) + |> element.to_string + + string.contains(rendered, "dir=\"ltr\"") + |> expect.to_equal(expected: True) }), ]), describe("styled direction", [ - it("styled helpers mirror headless direction semantics", fn() { + it("styled provider applies rtl dir and helper agrees", fn() { let t = theme.theme_default() let config = ui_direction.direction_provider_config( @@ -50,6 +67,9 @@ pub fn direction_tests() { string.contains(rendered, "dir=\"rtl\"") |> expect.to_equal(expected: True) + + string.contains(rendered, "dir=\"ltr\"") + |> expect.to_equal(expected: False) }), ]), ]) diff --git a/test/form_test.gleam b/test/form_test.gleam index 30afe30..d50ea57 100644 --- a/test/form_test.gleam +++ b/test/form_test.gleam @@ -33,6 +33,9 @@ pub fn form_tests() { rendered_form |> expect.to_equal(expected: rendered_forms) + + string.contains(rendered_form, " expect.to_equal(expected: True) }), ]), describe("styled form facade", [ @@ -64,19 +67,8 @@ pub fn form_tests() { rendered_form |> expect.to_equal(expected: rendered_forms) - }), - it("styled form root renders semantic form container", fn() { - let t = theme.theme_default() - let rendered = - ui_form.form( - theme: t, - config: ui_form.form_config(theme: t), - children: [weft_lustre.text(content: "body")], - ) - |> weft_lustre.layout(attrs: []) - |> element.to_string - string.contains(rendered, " expect.to_equal(expected: True) }), it("headless form helper wiring remains available", fn() { @@ -93,6 +85,9 @@ pub fn form_tests() { string.contains(rendered, " expect.to_equal(expected: True) + + string.contains(rendered, "id=\"bio\"") + |> expect.to_equal(expected: True) }), ]), ]) diff --git a/test/input_otp_test.gleam b/test/input_otp_test.gleam index f953bcc..3e09e37 100644 --- a/test/input_otp_test.gleam +++ b/test/input_otp_test.gleam @@ -9,8 +9,8 @@ import weft_lustre_ui/theme pub fn input_otp_tests() { describe("input_otp", [ - describe("headless config mutators", [ - it("input_otp config mutators affect rendered slot structure", fn() { + describe("headless behavior", [ + it("length and disabled mutators affect rendered slots", fn() { let config = headless_input_otp.input_otp_config( value: "12", @@ -29,6 +29,9 @@ pub fn input_otp_tests() { string.contains(rendered, "data-index=\"3\"") |> expect.to_equal(expected: True) + + string.contains(rendered, "data-index=\"4\"") + |> expect.to_equal(expected: False) }), ]), describe("headless rendering", [ @@ -59,8 +62,8 @@ pub fn input_otp_tests() { |> expect.to_equal(expected: True) }), ]), - describe("styled config mutators", [ - it("input_otp config mutators affect rendered slot structure", fn() { + describe("styled behavior", [ + it("length and disabled mutators affect styled rendered slots", fn() { let t = theme.theme_default() let config = ui_input_otp.input_otp_config( @@ -81,6 +84,9 @@ pub fn input_otp_tests() { string.contains(rendered, "data-index=\"3\"") |> expect.to_equal(expected: True) + + string.contains(rendered, "data-index=\"4\"") + |> expect.to_equal(expected: False) }), ]), describe("styled rendering", [ diff --git a/test/item_test.gleam b/test/item_test.gleam index a24a39e..4c9d7b3 100644 --- a/test/item_test.gleam +++ b/test/item_test.gleam @@ -10,7 +10,7 @@ import weft_lustre_ui/theme pub fn item_tests() { describe("item", [ describe("headless rendering", [ - it("headless config mutators update variant and size attributes", fn() { + it("headless config mutators render expected variant and size", fn() { let config = headless_item.item_config() |> headless_item.item_variant( @@ -45,8 +45,8 @@ pub fn item_tests() { |> expect.to_equal(expected: True) }), ]), - describe("styled config mutators", [ - it("item config round-trips through variant and size helpers", fn() { + describe("styled rendering", [ + it("styled item mutators render expected variant and size", fn() { let t = theme.theme_default() let config = ui_item.item_config(theme: t) @@ -56,32 +56,17 @@ pub fn item_tests() { ) |> ui_item.item_size(theme: t, size: ui_item.item_size_sm(theme: t)) - let variant = ui_item.item_config_variant(theme: t, config: config) - let size = ui_item.item_config_size(theme: t, config: config) - - ui_item.item_variant_is_outline(theme: t, variant: variant) - |> expect.to_equal(expected: True) + let rendered = + ui_item.item(theme: t, config: config, children: []) + |> weft_lustre.layout(attrs: []) + |> element.to_string - ui_item.item_size_is_sm(theme: t, size: size) + string.contains(rendered, "data-variant=\"outline\"") |> expect.to_equal(expected: True) - }), - it("item media config round-trips through media variant helpers", fn() { - let t = theme.theme_default() - let config = - ui_item.item_media_config(theme: t) - |> ui_item.item_media_variant( - theme: t, - variant: ui_item.item_media_variant_icon(theme: t), - ) - let variant = - ui_item.item_media_config_variant(theme: t, config: config) - - ui_item.item_media_variant_is_icon(theme: t, variant: variant) + string.contains(rendered, "data-size=\"sm\"") |> expect.to_equal(expected: True) }), - ]), - describe("styled rendering", [ it("renders item root and slots with expected data-slot markers", fn() { let t = theme.theme_default() let config = ui_item.item_config(theme: t) @@ -104,6 +89,9 @@ pub fn item_tests() { string.contains(rendered, "data-slot=\"item-content\"") |> expect.to_equal(expected: True) + + string.contains(rendered, "data-slot=\"item-title\"") + |> expect.to_equal(expected: True) }), ]), ]) diff --git a/test/menubar_test.gleam b/test/menubar_test.gleam index 87fed78..a018eec 100644 --- a/test/menubar_test.gleam +++ b/test/menubar_test.gleam @@ -17,8 +17,8 @@ fn open_change_label(open: Bool) -> String { pub fn menubar_tests() { describe("menubar", [ - describe("headless config mutators", [ - it("menu config mutators round-trip through rendered attributes", fn() { + describe("headless behavior", [ + it("menu config mutators render menu id/state and forwarded attrs", fn() { let menu_config = headless_menubar.menubar_menu_config(id: "file") |> headless_menubar.menubar_menu_open(open: True) @@ -47,40 +47,33 @@ pub fn menubar_tests() { string.contains(rendered, "data-extra=\"1\"") |> expect.to_equal(expected: True) }), - it("root config key mutators keep menubar renderable", fn() { - let root_config = - headless_menubar.menubar_config() - |> headless_menubar.menubar_on_move_prev(on_move_prev: "prev") - |> headless_menubar.menubar_on_move_next(on_move_next: "next") - |> headless_menubar.menubar_on_close_all(on_close_all: "close") - - let rendered = - weft_lustre.layout( - attrs: [], - child: headless_menubar.menubar(config: root_config, children: []), - ) - |> element.to_string - - string.contains(rendered, "data-slot=\"menubar\"") - |> expect.to_equal(expected: True) - }), - it("item config variant/inset helpers round-trip", fn() { - let config = - headless_menubar.menubar_item_config() - |> headless_menubar.menubar_item_variant( - variant: headless_menubar.menubar_item_variant_destructive(), - ) - |> headless_menubar.menubar_item_inset() - - let variant = - headless_menubar.menubar_item_config_variant(config: config) - - headless_menubar.menubar_item_variant_is_destructive(variant: variant) - |> expect.to_equal(expected: True) - - headless_menubar.menubar_item_config_inset(config: config) - |> expect.to_equal(expected: True) - }), + it( + "item mutators render inset, destructive variant, and disabled state", + fn() { + let config = + headless_menubar.menubar_item_config() + |> headless_menubar.menubar_item_variant( + variant: headless_menubar.menubar_item_variant_destructive(), + ) + |> headless_menubar.menubar_item_inset() + |> headless_menubar.menubar_item_disabled() + + let rendered = + headless_menubar.menubar_item( + config: config, + child: weft_lustre.text(content: "Delete"), + ) + |> weft_lustre.layout(attrs: []) + |> element.to_string + + string.contains(rendered, "data-variant=\"destructive\"") + |> expect.to_equal(expected: True) + string.contains(rendered, "data-inset=\"true\"") + |> expect.to_equal(expected: True) + string.contains(rendered, "data-disabled=\"true\"") + |> expect.to_equal(expected: True) + }, + ), ]), describe("headless rendering", [ it("renders menubar root and item slots", fn() { @@ -99,15 +92,15 @@ pub fn menubar_tests() { weft_lustre.layout(attrs: [], child: view) |> element.to_string - string.contains(rendered, "data-slot=\"menubar\"") + string.contains(rendered, "role=\"menubar\"") |> expect.to_equal(expected: True) string.contains(rendered, "data-slot=\"menubar-item\"") |> expect.to_equal(expected: True) }), ]), - describe("styled config mutators", [ - it("menu config mutators round-trip through rendered attributes", fn() { + describe("styled behavior", [ + it("styled menu config mutators render menu id/state and attrs", fn() { let t = theme.theme_default() let menu_config = ui_menubar.menubar_menu_config(theme: t, id: "file") @@ -139,50 +132,6 @@ pub fn menubar_tests() { string.contains(rendered, "data-extra=\"1\"") |> expect.to_equal(expected: True) }), - it("root config key mutators keep menubar renderable", fn() { - let t = theme.theme_default() - let root_config = - ui_menubar.menubar_config(theme: t) - |> ui_menubar.menubar_on_move_prev(theme: t, on_move_prev: "prev") - |> ui_menubar.menubar_on_move_next(theme: t, on_move_next: "next") - |> ui_menubar.menubar_on_close_all(theme: t, on_close_all: "close") - - let rendered = - weft_lustre.layout( - attrs: [], - child: ui_menubar.menubar( - theme: t, - config: root_config, - children: [], - ), - ) - |> element.to_string - - string.contains(rendered, "data-slot=\"menubar\"") - |> expect.to_equal(expected: True) - }), - it("item config variant/inset helpers round-trip", fn() { - let t = theme.theme_default() - let config = - ui_menubar.menubar_item_config(theme: t) - |> ui_menubar.menubar_item_variant( - theme: t, - variant: ui_menubar.menubar_item_variant_destructive(theme: t), - ) - |> ui_menubar.menubar_item_inset(theme: t) - - let variant = - ui_menubar.menubar_item_config_variant(theme: t, config: config) - - ui_menubar.menubar_item_variant_is_destructive( - theme: t, - variant: variant, - ) - |> expect.to_equal(expected: True) - - ui_menubar.menubar_item_config_inset(theme: t, config: config) - |> expect.to_equal(expected: True) - }), ]), describe("styled rendering", [ it("renders menubar root and item slots", fn() { @@ -203,7 +152,7 @@ pub fn menubar_tests() { weft_lustre.layout(attrs: [], child: view) |> element.to_string - string.contains(rendered, "data-slot=\"menubar\"") + string.contains(rendered, "role=\"menubar\"") |> expect.to_equal(expected: True) string.contains(rendered, "data-slot=\"menubar-item\"") diff --git a/test/navigation_menu_test.gleam b/test/navigation_menu_test.gleam index 01ffdca..36743a9 100644 --- a/test/navigation_menu_test.gleam +++ b/test/navigation_menu_test.gleam @@ -9,21 +9,38 @@ import weft_lustre_ui/theme pub fn navigation_menu_tests() { describe("navigation_menu", [ - describe("headless config mutators", [ - it("viewport-enabled flag round-trips through config helpers", fn() { + describe("headless behavior", [ + it("disabling viewport removes viewport slot and updates data flag", fn() { let config = headless_navigation_menu.navigation_menu_config() |> headless_navigation_menu.navigation_menu_viewport_enabled( enabled: False, ) - headless_navigation_menu.navigation_menu_config_viewport_enabled( - config: config, - ) + let rendered = + headless_navigation_menu.navigation_menu(config: config, children: []) + |> weft_lustre.layout(attrs: []) + |> element.to_string + + string.contains(rendered, "data-viewport=\"false\"") + |> expect.to_equal(expected: True) + + string.contains(rendered, "data-slot=\"navigation-menu-viewport\"") |> expect.to_equal(expected: False) }), - it("trigger style helper remains available", fn() { - { headless_navigation_menu.navigation_menu_trigger_style() != [] } + it("default config renders viewport and marks data-viewport true", fn() { + let rendered = + headless_navigation_menu.navigation_menu( + config: headless_navigation_menu.navigation_menu_config(), + children: [], + ) + |> weft_lustre.layout(attrs: []) + |> element.to_string + + string.contains(rendered, "data-viewport=\"true\"") + |> expect.to_equal(expected: True) + + string.contains(rendered, "data-slot=\"navigation-menu-viewport\"") |> expect.to_equal(expected: True) }), ]), @@ -65,12 +82,12 @@ pub fn navigation_menu_tests() { string.contains(rendered, "data-slot=\"navigation-menu-trigger\"") |> expect.to_equal(expected: True) - string.contains(rendered, "data-slot=\"navigation-menu-viewport\"") + string.contains(rendered, "type=\"button\"") |> expect.to_equal(expected: True) }), ]), - describe("styled config mutators", [ - it("viewport-enabled flag round-trips through config helpers", fn() { + describe("styled behavior", [ + it("styled config respects viewport enabled false", fn() { let t = theme.theme_default() let config = ui_navigation_menu.navigation_menu_config(theme: t) @@ -79,17 +96,20 @@ pub fn navigation_menu_tests() { enabled: False, ) - ui_navigation_menu.navigation_menu_config_viewport_enabled( - theme: t, - config: config, - ) - |> expect.to_equal(expected: False) - }), - it("trigger style helper remains available", fn() { - let t = theme.theme_default() + let rendered = + ui_navigation_menu.navigation_menu( + theme: t, + config: config, + children: [], + ) + |> weft_lustre.layout(attrs: []) + |> element.to_string - { ui_navigation_menu.navigation_menu_trigger_style(theme: t) != [] } + string.contains(rendered, "data-viewport=\"false\"") |> expect.to_equal(expected: True) + + string.contains(rendered, "data-slot=\"navigation-menu-viewport\"") + |> expect.to_equal(expected: False) }), ]), describe("styled rendering", [ @@ -140,7 +160,7 @@ pub fn navigation_menu_tests() { string.contains(rendered, "data-slot=\"navigation-menu\"") |> expect.to_equal(expected: True) - string.contains(rendered, "data-slot=\"navigation-menu-trigger\"") + string.contains(rendered, "data-slot=\"navigation-menu-link\"") |> expect.to_equal(expected: True) string.contains(rendered, "data-slot=\"navigation-menu-viewport\"") diff --git a/test/resizable_test.gleam b/test/resizable_test.gleam index d754a48..caea80f 100644 --- a/test/resizable_test.gleam +++ b/test/resizable_test.gleam @@ -9,35 +9,8 @@ import weft_lustre_ui/theme pub fn resizable_tests() { describe("resizable", [ - describe("headless config mutators", [ - it("panel group orientation helpers round-trip", fn() { - let vertical = headless_resizable.resizable_vertical() - - let group_config = - headless_resizable.resizable_panel_group_config() - |> headless_resizable.resizable_panel_group_orientation( - orientation: vertical, - ) - - let orientation = - headless_resizable.resizable_panel_group_config_orientation( - config: group_config, - ) - - headless_resizable.resizable_orientation_is_vertical( - orientation: orientation, - ) - |> expect.to_equal(expected: True) - }), - it("handle config exposes grip-affordance state", fn() { - let config = - headless_resizable.resizable_handle_config() - |> headless_resizable.resizable_handle_with_handle() - - headless_resizable.resizable_handle_config_with_handle(config: config) - |> expect.to_equal(expected: True) - }), - it("handle orientation can be derived from panel group orientation", fn() { + describe("headless behavior", [ + it("vertical panel group drives horizontal handle orientation", fn() { let group_config = headless_resizable.resizable_panel_group_config() |> headless_resizable.resizable_panel_group_orientation( @@ -50,20 +23,34 @@ pub fn resizable_tests() { group_config: group_config, ) - let orientation = - headless_resizable.resizable_handle_config_orientation( - config: handle_config, + let rendered = + headless_resizable.resizable_handle(config: handle_config) + |> weft_lustre.layout(attrs: []) + |> element.to_string + + string.contains(rendered, "aria-orientation=\"horizontal\"") + |> expect.to_equal(expected: True) + }), + it("horizontal panel group renders horizontal aria-orientation", fn() { + let rendered = + headless_resizable.resizable_panel_group( + config: headless_resizable.resizable_panel_group_config(), + children: [], ) + |> weft_lustre.layout(attrs: []) + |> element.to_string - headless_resizable.resizable_orientation_is_horizontal( - orientation: orientation, - ) + string.contains(rendered, "aria-orientation=\"horizontal\"") |> expect.to_equal(expected: True) }), ]), describe("headless rendering", [ it("renders panel group/panel/handle slots", fn() { - let group_config = headless_resizable.resizable_panel_group_config() + let group_config = + headless_resizable.resizable_panel_group_config() + |> headless_resizable.resizable_panel_group_orientation( + orientation: headless_resizable.resizable_vertical(), + ) let handle_config = headless_resizable.resizable_handle_config() |> headless_resizable.resizable_handle_with_handle() @@ -93,43 +80,37 @@ pub fn resizable_tests() { |> expect.to_equal(expected: True) }), ]), - describe("styled config mutators", [ - it("panel group orientation helpers round-trip", fn() { - let t = theme.theme_default() - let vertical = ui_resizable.resizable_vertical(theme: t) - - let group_config = - ui_resizable.resizable_panel_group_config(theme: t) - |> ui_resizable.resizable_panel_group_orientation( - theme: t, - orientation: vertical, - ) - - let orientation = - ui_resizable.resizable_panel_group_config_orientation( - theme: t, - config: group_config, - ) - - ui_resizable.resizable_orientation_is_vertical( - theme: t, - orientation: orientation, - ) - |> expect.to_equal(expected: True) - }), - it("handle config exposes grip-affordance state", fn() { - let t = theme.theme_default() - let config = - ui_resizable.resizable_handle_config(theme: t) - |> ui_resizable.resizable_handle_with_handle(theme: t) - - ui_resizable.resizable_handle_config_with_handle( - theme: t, - config: config, - ) - |> expect.to_equal(expected: True) - }), - it("handle orientation can be derived from panel group orientation", fn() { + describe("styled behavior", [ + it( + "styled vertical panel group drives horizontal handle orientation", + fn() { + let t = theme.theme_default() + let group_config = + ui_resizable.resizable_panel_group_config(theme: t) + |> ui_resizable.resizable_panel_group_orientation( + theme: t, + orientation: ui_resizable.resizable_vertical(theme: t), + ) + + let handle_config = + ui_resizable.resizable_handle_config(theme: t) + |> ui_resizable.resizable_handle_orientation_from_group( + theme: t, + group_config: group_config, + ) + + let rendered = + ui_resizable.resizable_handle(theme: t, config: handle_config) + |> weft_lustre.layout(attrs: []) + |> element.to_string + + string.contains(rendered, "aria-orientation=\"horizontal\"") + |> expect.to_equal(expected: True) + }, + ), + ]), + describe("styled rendering", [ + it("renders panel group/panel/handle slots", fn() { let t = theme.theme_default() let group_config = ui_resizable.resizable_panel_group_config(theme: t) @@ -137,31 +118,6 @@ pub fn resizable_tests() { theme: t, orientation: ui_resizable.resizable_vertical(theme: t), ) - - let handle_config = - ui_resizable.resizable_handle_config(theme: t) - |> ui_resizable.resizable_handle_orientation_from_group( - theme: t, - group_config: group_config, - ) - - let orientation = - ui_resizable.resizable_handle_config_orientation( - theme: t, - config: handle_config, - ) - - ui_resizable.resizable_orientation_is_horizontal( - theme: t, - orientation: orientation, - ) - |> expect.to_equal(expected: True) - }), - ]), - describe("styled rendering", [ - it("renders panel group/panel/handle slots", fn() { - let t = theme.theme_default() - let group_config = ui_resizable.resizable_panel_group_config(theme: t) let handle_config = ui_resizable.resizable_handle_config(theme: t) let view = diff --git a/test/textarea_test.gleam b/test/textarea_test.gleam index 9d3bdb7..685b81d 100644 --- a/test/textarea_test.gleam +++ b/test/textarea_test.gleam @@ -17,12 +17,16 @@ pub fn textarea_tests() { headless_input.textarea_config(value: "Hello", on_input: fn(_value) { "changed" }) + |> headless_input.textarea_rows(rows: 4) + |> headless_input.textarea_placeholder(value: "Tell us") let cfg_facade = headless_textarea.textarea_config( value: "Hello", on_input: fn(_value) { "changed" }, ) + |> headless_textarea.textarea_rows(rows: 4) + |> headless_textarea.textarea_placeholder(value: "Tell us") let rendered_input = headless_input.textarea(config: cfg_input) @@ -36,6 +40,11 @@ pub fn textarea_tests() { rendered_facade |> expect.to_equal(expected: rendered_input) + + string.contains(rendered_facade, "rows=\"4\"") + |> expect.to_equal(expected: True) + string.contains(rendered_facade, "placeholder=\"Tell us\"") + |> expect.to_equal(expected: True) }), ]), describe("styled textarea facade", [ @@ -49,12 +58,14 @@ pub fn textarea_tests() { on_input: fn(_value) { "changed" }, ) |> ui_textarea.textarea_rows(theme: t, rows: 5) + |> ui_textarea.textarea_disabled(theme: t) let cfg_input = ui_input.textarea_config(value: "Hello", on_input: fn(_value) { "changed" }) |> ui_input.textarea_rows(rows: 5) + |> ui_input.textarea_disabled() let rendered_facade = ui_textarea.textarea(theme: t, config: cfg_facade) @@ -71,6 +82,9 @@ pub fn textarea_tests() { string.contains(rendered_facade, "rows=\"5\"") |> expect.to_equal(expected: True) + + string.contains(rendered_facade, "disabled") + |> expect.to_equal(expected: True) }), ]), ]) From 1c72b7f5203f9d58d9c4393a3f1c8e0983319c6e Mon Sep 17 00:00:00 2001 From: Brett Bonner Date: Wed, 25 Feb 2026 21:03:36 -0800 Subject: [PATCH 2/3] Address remaining audit gaps in carousel and textarea tests --- docs/test_audit_headscratchers.md | 8 ++++++++ test/carousel_test.gleam | 17 ++++++++++++++++- test/textarea_test.gleam | 2 +- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/docs/test_audit_headscratchers.md b/docs/test_audit_headscratchers.md index f76c636..662c6ac 100644 --- a/docs/test_audit_headscratchers.md +++ b/docs/test_audit_headscratchers.md @@ -2,6 +2,14 @@ This pass focuses on tests that look like they may have been written to keep green status rather than to strongly verify behavior. + +## Current status (post-rewrite follow-up) + +- ✅ Addressed in tests: `navigation_menu`, `resizable`, `menubar`, `item`, `form`, `input_otp`, and `direction` now include behavior-oriented assertions that replaced the original low-signal round-trips. +- ✅ Addressed in this follow-up: `carousel` now checks `carousel-content` and `carousel-previous` slots in both headless and styled render paths, adds a negative orientation assertion (`horizontal` absent when `vertical` is configured), and tightens disabled checks to explicit boolean attribute rendering. +- ✅ Addressed in this follow-up: `textarea` disabled assertion is now precise (`disabled=""`) instead of a broad `"disabled"` token check. +- 🔄 Remaining long-term improvement opportunities (not blocking this audit close-out): replace broad string assertions with attribute extraction/parsing helpers where practical. + ## Heuristics used - **Self-fulfilling round-trips**: setter + getter + predicate all from same module, no independent oracle. diff --git a/test/carousel_test.gleam b/test/carousel_test.gleam index c724595..855c89b 100644 --- a/test/carousel_test.gleam +++ b/test/carousel_test.gleam @@ -52,7 +52,7 @@ pub fn carousel_tests() { |> weft_lustre.layout(attrs: []) |> element.to_string - string.contains(rendered, "disabled") + string.contains(rendered, "disabled=\"\"") |> expect.to_equal(expected: True) }), ]), @@ -90,6 +90,12 @@ pub fn carousel_tests() { string.contains(rendered, "data-slot=\"carousel-item\"") |> expect.to_equal(expected: True) + string.contains(rendered, "data-slot=\"carousel-content\"") + |> expect.to_equal(expected: True) + + string.contains(rendered, "data-slot=\"carousel-previous\"") + |> expect.to_equal(expected: True) + string.contains(rendered, "data-slot=\"carousel-next\"") |> expect.to_equal(expected: True) }), @@ -136,6 +142,9 @@ pub fn carousel_tests() { string.contains(rendered, "data-orientation=\"vertical\"") |> expect.to_equal(expected: True) + + string.contains(rendered, "data-orientation=\"horizontal\"") + |> expect.to_equal(expected: False) }), ]), describe("styled rendering", [ @@ -175,6 +184,12 @@ pub fn carousel_tests() { string.contains(rendered, "data-slot=\"carousel-item\"") |> expect.to_equal(expected: True) + string.contains(rendered, "data-slot=\"carousel-content\"") + |> expect.to_equal(expected: True) + + string.contains(rendered, "data-slot=\"carousel-previous\"") + |> expect.to_equal(expected: True) + string.contains(rendered, "data-slot=\"carousel-next\"") |> expect.to_equal(expected: True) }), diff --git a/test/textarea_test.gleam b/test/textarea_test.gleam index 685b81d..9951c3b 100644 --- a/test/textarea_test.gleam +++ b/test/textarea_test.gleam @@ -83,7 +83,7 @@ pub fn textarea_tests() { string.contains(rendered_facade, "rows=\"5\"") |> expect.to_equal(expected: True) - string.contains(rendered_facade, "disabled") + string.contains(rendered_facade, "disabled=\"\"") |> expect.to_equal(expected: True) }), ]), From 47970cba8afc1bc49f78985d530948c9a13cd647 Mon Sep 17 00:00:00 2001 From: Brett Bonner Date: Thu, 26 Feb 2026 08:55:40 -0800 Subject: [PATCH 3/3] Remove test audit document from PR scope --- docs/test_audit_headscratchers.md | 71 ------------------------------- 1 file changed, 71 deletions(-) delete mode 100644 docs/test_audit_headscratchers.md diff --git a/docs/test_audit_headscratchers.md b/docs/test_audit_headscratchers.md deleted file mode 100644 index 662c6ac..0000000 --- a/docs/test_audit_headscratchers.md +++ /dev/null @@ -1,71 +0,0 @@ -# Test Audit: Potentially "Fishy" / Low-Signal Tests - -This pass focuses on tests that look like they may have been written to keep green status rather than to strongly verify behavior. - - -## Current status (post-rewrite follow-up) - -- ✅ Addressed in tests: `navigation_menu`, `resizable`, `menubar`, `item`, `form`, `input_otp`, and `direction` now include behavior-oriented assertions that replaced the original low-signal round-trips. -- ✅ Addressed in this follow-up: `carousel` now checks `carousel-content` and `carousel-previous` slots in both headless and styled render paths, adds a negative orientation assertion (`horizontal` absent when `vertical` is configured), and tightens disabled checks to explicit boolean attribute rendering. -- ✅ Addressed in this follow-up: `textarea` disabled assertion is now precise (`disabled=""`) instead of a broad `"disabled"` token check. -- 🔄 Remaining long-term improvement opportunities (not blocking this audit close-out): replace broad string assertions with attribute extraction/parsing helpers where practical. - -## Heuristics used - -- **Self-fulfilling round-trips**: setter + getter + predicate all from same module, no independent oracle. -- **Wrapper-equals-wrapper tests**: compares façade output to canonical helper output but never validates user-observable behavior. -- **Weak markup checks**: only checks `string.contains` for a broad token (`data-slot=...`) that may pass even if behavior regresses. -- **"Function exists" checks**: assertions like `helper() != []` that only prove non-empty output, not correctness. - -## Head-scratcher list - -1. `test/navigation_menu_test.gleam` - - `trigger style helper remains available` (headless + styled) only checks non-empty style list (`... != []`), which is weak and likely to pass through regressions. - - `viewport-enabled flag round-trips through config helpers` (headless + styled) is setter/getter self-validation with no render/event consequence check. - - Rendering tests only assert a few `data-slot` markers and do not verify viewport toggling behavior when disabled. - -2. `test/carousel_test.gleam` - - `orientation helpers round-trip...` (headless + styled) uses module-local setter/getter/predicate chain; high risk of tautological pass. - - Rendering assertions only check 3 slot markers (`carousel`, `carousel-item`, `carousel-next`), missing prev state/disabled semantics/orientation-specific rendering effects. - -3. `test/resizable_test.gleam` - - Orientation and handle tests are mostly helper round-trips with little externally validated behavior. - - Render test expects `aria-orientation="vertical"` but does not explicitly set orientation in test setup, coupling to defaults and making intent brittle. - -4. `test/item_test.gleam` - - Styled mutator tests are round-trips through same API (`item_variant` -> `item_config_variant` -> `item_variant_is_*`) with no independent expected value check. - - Render tests only verify slot-marker presence and not semantic behavior. - -5. `test/menubar_test.gleam` - - `root config key mutators keep menubar renderable` (headless + styled) checks only that rendering still includes `data-slot="menubar"`; this can pass even if callbacks are ignored. - - Item variant/inset tests are helper round-trips with no UI consequence assertions. - -6. `test/form_test.gleam` - - Façade-forwarding tests compare rendered strings from two related APIs; useful for aliasing checks but weak for product behavior. - - `styled form root renders semantic form container` only checks `