""" Tests for the check-css-class-prefix pre-commit hook. Specifically guards against the regex regression fixed in PR #3103, where `(? subprocess.CompletedProcess: """Write content to a temp file and run the hook against it.""" with tempfile.TemporaryDirectory() as d: path = Path(d) / filename path.parent.mkdir(parents=True, exist_ok=True) path.write_text(content) return subprocess.run( [sys.executable, str(HOOK_SCRIPT), str(path)], capture_output=True, text=True, ) class TestCompoundSelectorsCSS: """The fix in PR #3103: every class in a compound selector must be checked.""" def test_compound_selector_first_class_unprefixed_is_caught(self): """`.bare.ldr-other` must fail on `.bare`.""" result = _run_hook(".bare.ldr-other { color: red; }\n", "x.css") assert result.returncode == 1 assert "'.bare'" in result.stdout def test_compound_selector_second_class_unprefixed_is_caught(self): """`.ldr-foo.selected` was previously silently passing — must fail now.""" result = _run_hook(".ldr-foo.selected { color: red; }\n", "x.css") assert result.returncode == 1 assert "'.selected'" in result.stdout def test_compound_selector_three_classes_all_validated(self): """`.ldr-a.bare.ldr-c` must fail on the middle bare class.""" result = _run_hook(".ldr-a.bare.ldr-c { color: red; }\n", "x.css") assert result.returncode == 1 assert "'.bare'" in result.stdout def test_compound_selector_all_prefixed_passes(self): """`.ldr-foo.ldr-bar` is valid — all classes have the prefix.""" result = _run_hook(".ldr-foo.ldr-bar { color: red; }\n", "x.css") assert result.returncode == 0 def test_compound_with_pseudo_class_validates_real_classes(self): """`.ldr-foo.bare:hover` must fail on `.bare`, not on `:hover`.""" result = _run_hook(".ldr-foo.bare:hover { color: red; }\n", "x.css") assert result.returncode == 1 assert "'.bare'" in result.stdout assert "'.hover'" not in result.stdout class TestDescendantSelectorsCSS: """Space-separated selectors must continue to validate each class.""" def test_descendant_selector_with_unprefixed_child_fails(self): result = _run_hook(".ldr-parent .bare { color: red; }\n", "x.css") assert result.returncode == 1 assert "'.bare'" in result.stdout def test_descendant_selector_all_prefixed_passes(self): result = _run_hook(".ldr-parent .ldr-child { color: red; }\n", "x.css") assert result.returncode == 0 class TestUrlsAreNotMatched: """The hook's lookbehind must continue to skip dots inside URL hostnames.""" def test_url_in_background_image_is_not_flagged(self): css = ( ".ldr-foo {\n" " background: url(https://www.example.com/spinner.svg);\n" "}\n" ) result = _run_hook(css, "x.css") assert result.returncode == 0 def test_unquoted_url_with_dotted_path_is_not_flagged(self): css = ".ldr-foo { background: url(www.w3.org/path); }\n" result = _run_hook(css, "x.css") assert result.returncode == 0 class TestAllowlistedClasses: """Bootstrap and other framework classes must pass the prefix check.""" @pytest.mark.parametrize( "selector", [ ".container", ".form-group", ".btn-primary", ".active", ".disabled", ".visually-hidden", ".bi-search", ".fa-spinner", ], ) def test_allowed_framework_class_passes(self, selector: str): result = _run_hook(f"{selector} {{ color: red; }}\n", "x.css") assert result.returncode == 0 def test_compound_with_allowlisted_modifier_passes(self): """`.ldr-foo.active` must pass — `active` is allowlisted.""" result = _run_hook(".ldr-foo.active { color: red; }\n", "x.css") assert result.returncode == 0 class TestCommentsAndImportsSkipped: """Lines that are CSS comments or @import should not be scanned.""" def test_unprefixed_class_inside_comment_is_ignored(self): css = "/* .bare-class is mentioned but not defined here */\n" result = _run_hook(css, "x.css") assert result.returncode == 0 def test_import_statement_is_skipped(self): css = '@import url("./other.bare-thing.css");\n' result = _run_hook(css, "x.css") assert result.returncode == 0 class TestStyleTagInHTML: """The HTML scanner uses the same regex inside \n" ) result = _run_hook(html, "x.html") assert result.returncode == 1 assert "'.bare'" in result.stdout assert "in style tag" in result.stdout def test_compound_selector_outside_style_tag_falls_through_to_attr_check( self, ): """A `.ldr-foo.bare` literal outside