624 lines
23 KiB
Python
624 lines
23 KiB
Python
from __future__ import annotations
|
|
|
|
import re
|
|
import unittest
|
|
from collections import defaultdict
|
|
from pathlib import Path
|
|
|
|
from tests.frontend_test_helpers import assembled_frontend_document
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
STATIC = ROOT / "frontend"
|
|
TOKENS = STATIC / "shared" / "tokens.css"
|
|
|
|
MODULE_STYLESHEETS = (
|
|
"shared/base.css",
|
|
"shared/shell.css",
|
|
"shared/auth.css",
|
|
"shared/components/controls.css",
|
|
"shared/components/navigation.css",
|
|
"shared/components/cards.css",
|
|
"shared/components/tables.css",
|
|
"shared/components/dialogs.css",
|
|
"shared/components/feedback.css",
|
|
"pages/market/foundation.css",
|
|
"pages/sentiment/foundation.css",
|
|
"pages/pools/foundation.css",
|
|
"pages/ladder/foundation.css",
|
|
"pages/rotation/foundation.css",
|
|
"pages/auction/foundation.css",
|
|
"pages/themes/foundation.css",
|
|
"pages/popularity/foundation.css",
|
|
"pages/dragon-tiger/foundation.css",
|
|
"pages/screener/foundation.css",
|
|
"pages/mentor/foundation.css",
|
|
"pages/heaven/foundation.css",
|
|
"pages/review/foundation.css",
|
|
)
|
|
|
|
RETIRED_STYLESHEETS = (
|
|
"styles/styles.css",
|
|
"styles/renovation.css",
|
|
"styles/redesign-v2.css",
|
|
"styles/design-system.css",
|
|
"styles/theme.css",
|
|
"pages/heaven/page.css",
|
|
)
|
|
|
|
PAGE_OWNERS = {
|
|
"sentimentCycleView": "pages/sentiment/foundation.css",
|
|
"limitPool": "pages/pools/foundation.css",
|
|
"brokenView": "pages/pools/foundation.css",
|
|
"downView": "pages/pools/foundation.css",
|
|
"yesterdayView": "pages/pools/foundation.css",
|
|
"performanceView": "pages/pools/foundation.css",
|
|
"ladderView": "pages/ladder/foundation.css",
|
|
"rotationView": "pages/rotation/foundation.css",
|
|
"auctionView": "pages/auction/foundation.css",
|
|
"themeLibraryView": "pages/themes/foundation.css",
|
|
"popularityView": "pages/popularity/foundation.css",
|
|
"dragonView": "pages/dragon-tiger/foundation.css",
|
|
"screenerView": "pages/screener/foundation.css",
|
|
"screenerTrackingView": "pages/screener/foundation.css",
|
|
"mentorView": "pages/mentor/foundation.css",
|
|
"heavenView": "pages/heaven/foundation.css",
|
|
"reviewWorkspaceView": "pages/review/foundation.css",
|
|
}
|
|
|
|
SHARED_ROOT_OWNERS = {
|
|
".app-header": "shared/shell.css",
|
|
".app-main": "shared/shell.css",
|
|
".module-nav": "shared/shell.css",
|
|
".overview-strip": "shared/shell.css",
|
|
".sidebar": "shared/shell.css",
|
|
".status-bar": "shared/shell.css",
|
|
".workspace-view": "shared/shell.css",
|
|
".button": "shared/components/controls.css",
|
|
".icon-button": "shared/components/controls.css",
|
|
".segment": "shared/components/navigation.css",
|
|
".data-table": "shared/components/tables.css",
|
|
".card": "shared/components/cards.css",
|
|
".dialog": "shared/components/dialogs.css",
|
|
".toast": "shared/components/feedback.css",
|
|
}
|
|
|
|
HISTORICAL_RUNTIME_CSS_BYTES = 861_341
|
|
HISTORICAL_SELECTOR_CONTEXT_RULES = 8_569
|
|
|
|
|
|
def normalize(value: str) -> str:
|
|
return re.sub(r"\s+", " ", re.sub(r"/\*.*?\*/", "", value, flags=re.DOTALL)).strip()
|
|
|
|
|
|
def normalize_context(value: str) -> str:
|
|
"""Normalize equivalent at-rule spellings before ownership checks."""
|
|
return re.sub(r"\s*([():,])\s*", r"\1", normalize(value))
|
|
|
|
|
|
def skip_space_and_comments(text: str, position: int) -> int:
|
|
while position < len(text):
|
|
if text[position].isspace():
|
|
position += 1
|
|
elif text.startswith("/*", position):
|
|
closing = text.find("*/", position + 2)
|
|
position = len(text) if closing < 0 else closing + 2
|
|
else:
|
|
break
|
|
return position
|
|
|
|
|
|
def find_delimiter(text: str, position: int) -> tuple[int, str]:
|
|
quote = ""
|
|
escaped = False
|
|
depth = 0
|
|
in_comment = False
|
|
while position < len(text):
|
|
char = text[position]
|
|
pair = text[position : position + 2]
|
|
if in_comment:
|
|
if pair == "*/":
|
|
in_comment = False
|
|
position += 2
|
|
continue
|
|
position += 1
|
|
continue
|
|
if quote:
|
|
if escaped:
|
|
escaped = False
|
|
elif char == "\\":
|
|
escaped = True
|
|
elif char == quote:
|
|
quote = ""
|
|
position += 1
|
|
continue
|
|
if pair == "/*":
|
|
in_comment = True
|
|
position += 2
|
|
continue
|
|
if char in "\"'":
|
|
quote = char
|
|
elif char in "([":
|
|
depth += 1
|
|
elif char in ")]":
|
|
depth = max(0, depth - 1)
|
|
elif depth == 0 and char in "{;":
|
|
return position, char
|
|
position += 1
|
|
return len(text), ""
|
|
|
|
|
|
def find_closing_brace(text: str, opening: int) -> int:
|
|
depth = 1
|
|
position = opening + 1
|
|
quote = ""
|
|
escaped = False
|
|
in_comment = False
|
|
while position < len(text):
|
|
char = text[position]
|
|
pair = text[position : position + 2]
|
|
if in_comment:
|
|
if pair == "*/":
|
|
in_comment = False
|
|
position += 2
|
|
continue
|
|
position += 1
|
|
continue
|
|
if quote:
|
|
if escaped:
|
|
escaped = False
|
|
elif char == "\\":
|
|
escaped = True
|
|
elif char == quote:
|
|
quote = ""
|
|
position += 1
|
|
continue
|
|
if pair == "/*":
|
|
in_comment = True
|
|
position += 2
|
|
continue
|
|
if char in "\"'":
|
|
quote = char
|
|
elif char == "{":
|
|
depth += 1
|
|
elif char == "}":
|
|
depth -= 1
|
|
if depth == 0:
|
|
return position
|
|
position += 1
|
|
raise AssertionError(f"unclosed CSS block at offset {opening}")
|
|
|
|
|
|
def split_selectors(prelude: str) -> list[str]:
|
|
selectors: list[str] = []
|
|
start = 0
|
|
quote = ""
|
|
escaped = False
|
|
depth = 0
|
|
for index, char in enumerate(prelude):
|
|
if quote:
|
|
if escaped:
|
|
escaped = False
|
|
elif char == "\\":
|
|
escaped = True
|
|
elif char == quote:
|
|
quote = ""
|
|
continue
|
|
if char in "\"'":
|
|
quote = char
|
|
elif char in "([":
|
|
depth += 1
|
|
elif char in ")]":
|
|
depth = max(0, depth - 1)
|
|
elif char == "," and depth == 0:
|
|
selectors.append(normalize(prelude[start:index]))
|
|
start = index + 1
|
|
selectors.append(normalize(prelude[start:]))
|
|
return [selector for selector in selectors if selector]
|
|
|
|
|
|
def stylesheet_rule_keys(text: str, contexts: tuple[str, ...] = ()) -> list[tuple[tuple[str, ...], str]]:
|
|
keys: list[tuple[tuple[str, ...], str]] = []
|
|
position = 0
|
|
while True:
|
|
position = skip_space_and_comments(text, position)
|
|
if position >= len(text):
|
|
return keys
|
|
delimiter, kind = find_delimiter(text, position)
|
|
prelude = normalize(text[position:delimiter])
|
|
if not kind:
|
|
raise AssertionError(f"unparsed CSS tail: {prelude[:80]}")
|
|
if kind == ";":
|
|
position = delimiter + 1
|
|
continue
|
|
closing = find_closing_brace(text, delimiter)
|
|
body = text[delimiter + 1 : closing]
|
|
lowered = prelude.lower()
|
|
if lowered.startswith(("@media", "@supports", "@container", "@layer", "@starting-style", "@scope")):
|
|
keys.extend(stylesheet_rule_keys(body, contexts + (normalize_context(prelude),)))
|
|
elif lowered.startswith(("@keyframes", "@-webkit-keyframes")):
|
|
keys.append((contexts, prelude))
|
|
elif not prelude.startswith("@"):
|
|
keys.extend((contexts, selector) for selector in split_selectors(prelude))
|
|
position = closing + 1
|
|
|
|
|
|
def declaration_properties(body: str) -> frozenset[str]:
|
|
properties: set[str] = set()
|
|
start = 0
|
|
position = 0
|
|
quote = ""
|
|
escaped = False
|
|
depth = 0
|
|
in_comment = False
|
|
while position <= len(body):
|
|
char = body[position] if position < len(body) else ";"
|
|
pair = body[position : position + 2]
|
|
if in_comment:
|
|
if pair == "*/":
|
|
in_comment = False
|
|
position += 2
|
|
continue
|
|
position += 1
|
|
continue
|
|
if quote:
|
|
if escaped:
|
|
escaped = False
|
|
elif char == "\\":
|
|
escaped = True
|
|
elif char == quote:
|
|
quote = ""
|
|
position += 1
|
|
continue
|
|
if pair == "/*":
|
|
in_comment = True
|
|
position += 2
|
|
continue
|
|
if char in "\"'":
|
|
quote = char
|
|
elif char in "([":
|
|
depth += 1
|
|
elif char in ")]":
|
|
depth = max(0, depth - 1)
|
|
elif char == ";" and depth == 0:
|
|
declaration = normalize(body[start:position])
|
|
match = re.match(r"^(--[-a-zA-Z0-9_]+|[-a-zA-Z][-_a-zA-Z0-9]*)\s*:", declaration)
|
|
if match:
|
|
properties.add(match.group(1).lower())
|
|
start = position + 1
|
|
position += 1
|
|
return frozenset(properties)
|
|
|
|
|
|
def stylesheet_rules(
|
|
text: str,
|
|
contexts: tuple[str, ...] = (),
|
|
) -> list[tuple[tuple[str, ...], str, frozenset[str]]]:
|
|
rules: list[tuple[tuple[str, ...], str, frozenset[str]]] = []
|
|
position = 0
|
|
while True:
|
|
position = skip_space_and_comments(text, position)
|
|
if position >= len(text):
|
|
return rules
|
|
delimiter, kind = find_delimiter(text, position)
|
|
prelude = normalize(text[position:delimiter])
|
|
if not kind:
|
|
raise AssertionError(f"unparsed CSS tail: {prelude[:80]}")
|
|
if kind == ";":
|
|
position = delimiter + 1
|
|
continue
|
|
closing = find_closing_brace(text, delimiter)
|
|
body = text[delimiter + 1 : closing]
|
|
lowered = prelude.lower()
|
|
if lowered.startswith(("@media", "@supports", "@container", "@layer", "@starting-style", "@scope")):
|
|
rules.extend(stylesheet_rules(body, contexts + (normalize_context(prelude),)))
|
|
elif not prelude.startswith("@"):
|
|
properties = declaration_properties(body)
|
|
rules.extend((contexts, selector, properties) for selector in split_selectors(prelude))
|
|
position = closing + 1
|
|
|
|
|
|
def remove_page_scope(selector: str, view_id: str) -> str:
|
|
prefixes = (
|
|
rf":where\(#{re.escape(view_id)}\)\s+",
|
|
rf"#{re.escape(view_id)}\s+",
|
|
rf"body\[data-active-view=[\"']{re.escape(view_id)}[\"']\]\s+",
|
|
)
|
|
semantic = selector
|
|
for prefix in prefixes:
|
|
semantic = re.sub(rf"^(?P<theme>:root(?:\[[^\]]+\])?\s+)?{prefix}", r"\g<theme>", semantic)
|
|
return normalize(semantic)
|
|
|
|
|
|
def find_matching_parenthesis(value: str, opening: int) -> int:
|
|
depth = 1
|
|
quote = ""
|
|
escaped = False
|
|
for position in range(opening + 1, len(value)):
|
|
char = value[position]
|
|
if quote:
|
|
if escaped:
|
|
escaped = False
|
|
elif char == "\\":
|
|
escaped = True
|
|
elif char == quote:
|
|
quote = ""
|
|
continue
|
|
if char in "\"'":
|
|
quote = char
|
|
elif char == "(":
|
|
depth += 1
|
|
elif char == ")":
|
|
depth -= 1
|
|
if depth == 0:
|
|
return position
|
|
raise AssertionError(f"unclosed selector function: {value}")
|
|
|
|
|
|
def prune_selector_consumers(
|
|
selector: str,
|
|
source: str,
|
|
dynamic_prefixes: set[str],
|
|
) -> tuple[str, bool, bool]:
|
|
output: list[str] = []
|
|
cursor = 0
|
|
changed = False
|
|
function_pattern = re.compile(r":(is|where|has|not)\(", re.IGNORECASE)
|
|
while match := function_pattern.search(selector, cursor):
|
|
opening = match.end() - 1
|
|
closing = find_matching_parenthesis(selector, opening)
|
|
output.append(selector[cursor : match.start()])
|
|
name = match.group(1).lower()
|
|
content = selector[opening + 1 : closing]
|
|
arguments = split_selectors(content)
|
|
retained: list[str] = []
|
|
nested_changed = False
|
|
for argument in arguments:
|
|
pruned, reachable, argument_changed = prune_selector_consumers(argument, source, dynamic_prefixes)
|
|
nested_changed |= argument_changed
|
|
if reachable:
|
|
retained.append(pruned)
|
|
else:
|
|
nested_changed = True
|
|
if name != "not" and not retained:
|
|
return selector, False, True
|
|
if name == "not" and not retained:
|
|
changed = True
|
|
elif nested_changed or len(retained) != len(arguments):
|
|
output.append(f":{name}({', '.join(retained)})")
|
|
changed = True
|
|
else:
|
|
output.append(selector[match.start() : closing + 1])
|
|
cursor = closing + 1
|
|
output.append(selector[cursor:])
|
|
pruned_selector = "".join(output)
|
|
tokens = {
|
|
match.group(2)
|
|
for match in re.finditer(r"([.#])([_a-zA-Z][-_a-zA-Z0-9]*)", pruned_selector)
|
|
}
|
|
reachable = all(
|
|
token in source or any(token.startswith(prefix) for prefix in dynamic_prefixes)
|
|
for token in tokens
|
|
)
|
|
return pruned_selector, reachable, changed or not reachable
|
|
|
|
|
|
class CssGovernanceTests(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls) -> None:
|
|
cls.html = assembled_frontend_document()
|
|
cls.tokens = TOKENS.read_text(encoding="utf-8")
|
|
cls.modules = {
|
|
relative: (STATIC / relative).read_text(encoding="utf-8")
|
|
for relative in MODULE_STYLESHEETS
|
|
}
|
|
cls.rule_keys = {
|
|
relative: stylesheet_rule_keys(stylesheet)
|
|
for relative, stylesheet in cls.modules.items()
|
|
}
|
|
cls.rules = {
|
|
relative: stylesheet_rules(stylesheet)
|
|
for relative, stylesheet in cls.modules.items()
|
|
}
|
|
cls.runtime_source = "\n".join(
|
|
path.read_text(encoding="utf-8")
|
|
for path in STATIC.rglob("*")
|
|
if path.is_file()
|
|
and path.suffix in {".html", ".js", ".mjs"}
|
|
and "_css_core_before" not in path.parts
|
|
and path.name != "css-core-before.html"
|
|
)
|
|
cls.dynamic_class_prefixes = {
|
|
*re.findall(r"([_a-zA-Z][-_a-zA-Z0-9]*)\$\{", cls.runtime_source),
|
|
*re.findall(r'["\']([_a-zA-Z][-_a-zA-Z0-9]*)["\']\s*\+', cls.runtime_source),
|
|
}
|
|
|
|
def test_runtime_loads_only_the_canonical_stylesheet_stack(self) -> None:
|
|
expected = ("shared/tokens.css", *MODULE_STYLESHEETS)
|
|
positions = [self.html.index(f'/{path}') for path in expected]
|
|
self.assertEqual(positions, sorted(positions))
|
|
self.assertEqual(self.html.count('<link rel="stylesheet"'), len(expected))
|
|
for path in expected:
|
|
self.assertEqual(self.html.count(f'/{path}'), 1, path)
|
|
|
|
def test_historical_layers_and_patch_file_names_are_absent(self) -> None:
|
|
for relative in RETIRED_STYLESHEETS:
|
|
self.assertFalse((STATIC / relative).exists(), relative)
|
|
self.assertNotIn(f'/{relative}', self.html)
|
|
for name in ("legacy.css", "override.css", "fix.css"):
|
|
self.assertFalse(any(STATIC.rglob(name)), name)
|
|
|
|
def test_every_module_declares_its_owner_and_is_nonempty(self) -> None:
|
|
for relative, stylesheet in self.modules.items():
|
|
owner = Path(relative).parent.name if "/pages/" in f"/{relative}" else Path(relative).stem
|
|
if relative.startswith("shared/components/"):
|
|
owner = Path(relative).stem
|
|
self.assertTrue(
|
|
stylesheet.startswith(
|
|
f"/* Canonical CSS owner: {owner}. Historical layers consolidated 2026-08-02. */"
|
|
),
|
|
relative,
|
|
)
|
|
self.assertIn("{", stylesheet, relative)
|
|
|
|
def test_modules_do_not_contain_empty_declarations(self) -> None:
|
|
for relative, stylesheet in self.modules.items():
|
|
self.assertNotRegex(stylesheet, r"(?m)^\s*[-_a-zA-Z0-9]+:\s*;\s*$", relative)
|
|
|
|
def test_selector_and_context_have_exactly_one_owner(self) -> None:
|
|
owners: dict[tuple[tuple[str, ...], str], list[str]] = defaultdict(list)
|
|
for relative, keys in self.rule_keys.items():
|
|
for key in keys:
|
|
owners[key].append(relative)
|
|
duplicates = {
|
|
f"{' > '.join(contexts) or '<root>'} :: {selector}": paths
|
|
for (contexts, selector), paths in owners.items()
|
|
if len(paths) != 1
|
|
}
|
|
self.assertEqual(duplicates, {})
|
|
|
|
def test_page_ids_only_appear_in_their_owner_stylesheet(self) -> None:
|
|
for relative, stylesheet in self.modules.items():
|
|
for view_id, owner in PAGE_OWNERS.items():
|
|
if re.search(rf"(?:#{re.escape(view_id)}|data-active-view=[\"']{re.escape(view_id)})", stylesheet):
|
|
self.assertIn(relative, (owner, "shared/shell.css"), f"{view_id} leaked into {relative}")
|
|
|
|
def test_unscoped_shared_roots_stay_in_their_shared_owner(self) -> None:
|
|
for relative, keys in self.rule_keys.items():
|
|
for contexts, selector in keys:
|
|
owner = SHARED_ROOT_OWNERS.get(selector)
|
|
page_scoped = any(any(f"#{view_id}" in context for view_id in PAGE_OWNERS) for context in contexts)
|
|
if owner and not page_scoped:
|
|
self.assertEqual(relative, owner, f"{selector} leaked into {relative}")
|
|
|
|
def test_historical_shell_aliases_cannot_return(self) -> None:
|
|
class_tokens = {
|
|
token
|
|
for value in re.findall(r'class=["\']([^"\']*)["\']', self.html)
|
|
for token in value.split()
|
|
}
|
|
self.assertTrue({"app-header", "module-nav", "overview-strip", "status-bar"} <= class_tokens)
|
|
self.assertTrue({"topbar", "statusbar", "mktstrip", "brand", "logo"}.isdisjoint(class_tokens))
|
|
selector_text = "\n".join(
|
|
selector
|
|
for keys in self.rule_keys.values()
|
|
for _contexts, selector in keys
|
|
)
|
|
for alias in ("topbar", "statusbar", "mktstrip"):
|
|
self.assertNotRegex(selector_text, rf"(?<![-_a-zA-Z0-9])\.{alias}(?![-_a-zA-Z0-9])")
|
|
|
|
def test_special_shared_and_page_rules_have_one_owner(self) -> None:
|
|
exact_owners = {
|
|
".lucide": "shared/components/controls.css",
|
|
".visually-hidden": "shared/base.css",
|
|
}
|
|
for selector, expected_owner in exact_owners.items():
|
|
owners = [
|
|
relative
|
|
for relative, keys in self.rule_keys.items()
|
|
if any(candidate == selector for _contexts, candidate in keys)
|
|
]
|
|
self.assertEqual(owners, [expected_owner], selector)
|
|
|
|
for relative, keys in self.rule_keys.items():
|
|
for _contexts, selector in keys:
|
|
if ".wentian-v2-dialog" in selector:
|
|
self.assertEqual(relative, "pages/heaven/foundation.css", selector)
|
|
if re.search(r"\.admin-dialog\s+\.model-", selector):
|
|
self.assertEqual(relative, "shared/components/dialogs.css", selector)
|
|
|
|
def test_page_scope_variants_never_override_the_same_property(self) -> None:
|
|
conflicts: dict[str, list[str]] = defaultdict(list)
|
|
for view_id, owner in PAGE_OWNERS.items():
|
|
variants: dict[tuple[tuple[str, ...], str], list[tuple[str, frozenset[str]]]] = defaultdict(list)
|
|
for contexts, selector, properties in self.rules[owner]:
|
|
semantic = remove_page_scope(selector, view_id)
|
|
if semantic != selector:
|
|
variants[(contexts, semantic)].append((selector, properties))
|
|
for (contexts, semantic), rows in variants.items():
|
|
unique_selectors = {selector for selector, _properties in rows}
|
|
if len(unique_selectors) < 2:
|
|
continue
|
|
for index, (left_selector, left_properties) in enumerate(rows):
|
|
for right_selector, right_properties in rows[index + 1 :]:
|
|
if left_selector == right_selector:
|
|
continue
|
|
overlap = sorted(left_properties & right_properties)
|
|
if overlap:
|
|
context = " > ".join(contexts) or "<root>"
|
|
conflicts[owner].append(
|
|
f"{context} :: {semantic} :: {left_selector} <> {right_selector}: {overlap}"
|
|
)
|
|
details = "\n".join(
|
|
f"{owner}: {detail}"
|
|
for owner, owner_conflicts in conflicts.items()
|
|
for detail in owner_conflicts
|
|
)
|
|
self.assertFalse(conflicts, details)
|
|
|
|
def test_every_selector_has_a_runtime_consumer(self) -> None:
|
|
stale: list[str] = []
|
|
for relative, keys in self.rule_keys.items():
|
|
for contexts, selector in keys:
|
|
if selector.lower().startswith(("@keyframes", "@-webkit-keyframes")):
|
|
continue
|
|
_pruned, reachable, changed = prune_selector_consumers(
|
|
selector,
|
|
self.runtime_source,
|
|
self.dynamic_class_prefixes,
|
|
)
|
|
if not reachable or changed:
|
|
context = " > ".join(contexts) or "<root>"
|
|
stale.append(f"{relative}: {context} :: {selector}")
|
|
self.assertEqual(stale, [])
|
|
|
|
def test_runtime_css_is_smaller_and_has_fewer_rules(self) -> None:
|
|
total_bytes = sum((STATIC / path).stat().st_size for path in MODULE_STYLESHEETS)
|
|
total_rules = sum(len(keys) for keys in self.rule_keys.values())
|
|
self.assertLess(total_bytes, HISTORICAL_RUNTIME_CSS_BYTES)
|
|
self.assertLess(total_rules, HISTORICAL_SELECTOR_CONTEXT_RULES)
|
|
|
|
def test_token_file_has_three_layer_contract(self) -> None:
|
|
for heading in (
|
|
"/* Primitive tokens */",
|
|
"/* Semantic tokens */",
|
|
"/* Component tokens */",
|
|
"/* Compatibility aliases.",
|
|
):
|
|
self.assertIn(heading, self.tokens)
|
|
for variable in (
|
|
"--color-action:",
|
|
"--surface-canvas:",
|
|
"--text-primary:",
|
|
"--card-bg:",
|
|
"--control-height:",
|
|
"--sidebar-width:",
|
|
):
|
|
self.assertIn(variable, self.tokens)
|
|
|
|
def test_light_and_dark_semantics_share_one_owner(self) -> None:
|
|
self.assertIn(':root[data-theme="dark"] {', self.tokens)
|
|
global_root = re.compile(r'(?m)^:root(?:\[data-theme="dark"\])?\s*\{')
|
|
for filename, stylesheet in self.modules.items():
|
|
self.assertIsNone(global_root.search(stylesheet), filename)
|
|
|
|
def test_wentian_tokens_remain_isolated(self) -> None:
|
|
self.assertNotRegex(self.tokens, r"--wt-[a-z0-9-]+\s*:")
|
|
wentian = self.modules["pages/heaven/foundation.css"]
|
|
self.assertRegex(wentian, r"--wt-[a-z0-9-]+\s*:")
|
|
|
|
def test_compatibility_aliases_cover_historical_layers(self) -> None:
|
|
for variable in (
|
|
"--blue:",
|
|
"--up:",
|
|
"--xb-blue-500:",
|
|
"--r2-blue:",
|
|
"--chart-background:",
|
|
"--dragon-profile-list-width:",
|
|
):
|
|
self.assertIn(variable, self.tokens)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|