""" Every `:hover` rule in style.css is inside `@media (hover: hover)`. A tap on a touchscreen puts the element in `:hover` and nothing takes it out until another element is tapped, so a button tapped once kept its hover colour as if still pressed. It was fixed for the music player's buttons alone, then reported again on the pager arrows, the nav hamburger and the rest: a rule per button only moves the bug to the next button. This holds the whole file. """ from pathlib import Path import pytest STYLE = (Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static" / "style.css") pytestmark = pytest.mark.skipif(not STYLE.exists(), reason="the stylesheet is not in this checkout") def _hover_rules(source: str) -> list[tuple[int, list[str], str]]: """(line, enclosing at-rule preludes, selector) for each rule naming :hover.""" found = [] stack: list[str] = [] start = 0 i = 0 while i < len(source): if source.startswith("/*", i): i = source.index("*/", i) + 2 start = i continue c = source[i] if c == "{": prelude = source[start:i].strip() if ":hover" in prelude and not prelude.startswith("@"): found.append((source.count("\n", 0, i) + 1, list(stack), prelude)) stack.append(prelude) start = i + 1 elif c == "}": stack.pop() start = i + 1 elif c == ";": start = i + 1 i += 1 return found def test_every_hover_rule_is_scoped_to_devices_that_hover(): rules = _hover_rules(STYLE.read_text(encoding="utf-8")) assert rules, "no :hover rule found, did the parser break?" loose = [f"line {line}: {sel}" for line, stack, sel in rules if not any("(hover: hover)" in at for at in stack)] assert not loose, ("wrap these in @media (hover: hover), a tap leaves them " "stuck on a touchscreen:\n" + "\n".join(loose)) def test_the_parser_catches_a_loose_rule(): rules = _hover_rules("/* a { } */ .a:hover { color: red; }\n" "@media (hover: hover) { .b:hover { color: red; } }") assert [(sel, bool(stack)) for _, stack, sel in rules] == [ (".a:hover", False), (".b:hover", True)]