|
@@ -0,0 +1,391 @@
|
|
|
|
|
+#!/usr/bin/env python3
|
|
|
|
|
+"""Spry CSS regression checks — the visual contracts of the framework.
|
|
|
|
|
+
|
|
|
|
|
+Parses the shipped stylesheets as text (no browser needed) and asserts the
|
|
|
|
|
+shape the goal work established:
|
|
|
|
|
+
|
|
|
|
|
+ * Airy's dialog glide-in really is the Win7 DWM Aero curve: the open ease
|
|
|
|
|
+ numerically equals f(t) = 1-(1-t)^3 and the close ease equals t^3,
|
|
|
|
|
+ duration 180 ms, scale-from-0.90..0.95, anchored popovers translating
|
|
|
|
|
+ toward their trigger.
|
|
|
|
|
+ * Core padding defaults: bare `.card` and bare `dialog`/`[popover]` carry
|
|
|
|
|
+ padding by default, with :has() zero-guards so structural children stay
|
|
|
|
|
+ flush to the frame.
|
|
|
|
|
+ * Every theme keeps defining the tokens the core now relies on.
|
|
|
|
|
+ * The demo page (css/index.html) links and switches everything it demos.
|
|
|
|
|
+ * Formal's heading scale opts out of .prose on BOTH activation routes.
|
|
|
|
|
+ * Brace/paren balance and no tabs across all shipped CSS.
|
|
|
|
|
+ * README points at the live demo, not stale paths.
|
|
|
|
|
+
|
|
|
|
|
+Plain stdlib, no pytest: prints PASS/FAIL lines and a final summary in the
|
|
|
|
|
+spry-tests style ("---- N passed, M failed ----"), exits non-zero on any
|
|
|
|
|
+failure. Run from anywhere:
|
|
|
|
|
+
|
|
|
|
|
+ python3 tests/css_check.py
|
|
|
|
|
+"""
|
|
|
|
|
+
|
|
|
|
|
+import re
|
|
|
|
|
+import sys
|
|
|
|
|
+from pathlib import Path
|
|
|
|
|
+
|
|
|
|
|
+ROOT = Path(__file__).resolve().parent.parent
|
|
|
|
|
+
|
|
|
|
|
+THEMES = ["airy", "dwarf", "formal", "glassy", "h2o", "living-sky", "modern"]
|
|
|
|
|
+
|
|
|
|
|
+DEMO_MARKERS = [
|
|
|
|
|
+ "popover-anchor",
|
|
|
|
|
+ "dialog-titlebar",
|
|
|
|
|
+ "drawer",
|
|
|
|
|
+ "expander-group",
|
|
|
|
|
+ "otp",
|
|
|
|
|
+ "table-wrap",
|
|
|
|
|
+ "tabs",
|
|
|
|
|
+]
|
|
|
|
|
+
|
|
|
|
|
+passes = 0
|
|
|
|
|
+failures = 0
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def check(condition, label, detail=None):
|
|
|
|
|
+ global passes, failures
|
|
|
|
|
+ if condition:
|
|
|
|
|
+ passes += 1
|
|
|
|
|
+ print("PASS %s" % label)
|
|
|
|
|
+ else:
|
|
|
|
|
+ failures += 1
|
|
|
|
|
+ print("FAIL %s" % label)
|
|
|
|
|
+ if detail:
|
|
|
|
|
+ print(" %s" % detail)
|
|
|
|
|
+ return bool(condition)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+# --------------------------------------------------------------------------
|
|
|
|
|
+# Text helpers
|
|
|
|
|
+# --------------------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+def read(rel):
|
|
|
|
|
+ return (ROOT / rel).read_text(encoding="utf-8")
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def strip_comments(css):
|
|
|
|
|
+ return re.sub(r"/\*.*?\*/", "", css, flags=re.S)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def collapse_ws(text):
|
|
|
|
|
+ return re.sub(r"\s+", " ", text).strip()
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def iter_blocks(css):
|
|
|
|
|
+ """Yield (selector, body) for every top-level `{}` block, brace-matched."""
|
|
|
|
|
+ depth = 0
|
|
|
|
|
+ selector_start = 0
|
|
|
|
|
+ body_start = 0
|
|
|
|
|
+ for i, ch in enumerate(css):
|
|
|
|
|
+ if ch == "{":
|
|
|
|
|
+ if depth == 0:
|
|
|
|
|
+ selector = css[selector_start:i]
|
|
|
|
|
+ body_start = i + 1
|
|
|
|
|
+ depth += 1
|
|
|
|
|
+ elif ch == "}":
|
|
|
|
|
+ depth -= 1
|
|
|
|
|
+ if depth == 0:
|
|
|
|
|
+ yield selector, css[body_start:i]
|
|
|
|
|
+ selector_start = i + 1
|
|
|
|
|
+ elif depth == 0 and ch == ";":
|
|
|
|
|
+ # a declaration at top level ends any pseudo-selector window
|
|
|
|
|
+ selector_start = i + 1
|
|
|
|
|
+ if depth != 0:
|
|
|
|
|
+ raise ValueError("unbalanced braces while scanning blocks")
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def find_block(css, selector_regex):
|
|
|
|
|
+ """First top-level block whose selector matches; returns (selector, body)."""
|
|
|
|
|
+ rx = re.compile(selector_regex)
|
|
|
|
|
+ for selector, body in iter_blocks(css):
|
|
|
|
|
+ if rx.search(collapse_ws(selector)):
|
|
|
|
|
+ return selector, body
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def token_value(css, name, occurrence=0):
|
|
|
|
|
+ """Nth definition (0-based) of a custom property, or None."""
|
|
|
|
|
+ matches = re.findall(r"%s:\s*([^;]+);" % re.escape(name), css)
|
|
|
|
|
+ return matches[occurrence].strip() if len(matches) > occurrence else None
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+# --------------------------------------------------------------------------
|
|
|
|
|
+# a. Bezier math — Airy's glide-in must be the DWM curve
|
|
|
|
|
+# --------------------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+BEZIER_RX = re.compile(
|
|
|
|
|
+ r"cubic-bezier\(\s*(-?[\d.]+)\s*,\s*(-?[\d.]+)\s*,\s*(-?[\d.]+)\s*,\s*(-?[\d.]+)\s*\)"
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def parse_bezier(value):
|
|
|
|
|
+ m = BEZIER_RX.fullmatch(value.strip())
|
|
|
|
|
+ if not m:
|
|
|
|
|
+ return None
|
|
|
|
|
+ return tuple(float(g) for g in m.groups())
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def bezier_y(x1, y1, x2, y2, t):
|
|
|
|
|
+ """y(t) of a CSS cubic-bezier: solve x(s)=t for s (x is monotonic for
|
|
|
|
|
+ x1,x2 in [0,1]) by bisection, then evaluate y(s)."""
|
|
|
|
|
+ def coord(a, b, s):
|
|
|
|
|
+ return 3 * (1 - s) ** 2 * s * a + 3 * (1 - s) * s * s * b + s ** 3
|
|
|
|
|
+ if t <= 0.0:
|
|
|
|
|
+ return 0.0
|
|
|
|
|
+ if t >= 1.0:
|
|
|
|
|
+ return 1.0
|
|
|
|
|
+ lo, hi = 0.0, 1.0
|
|
|
|
|
+ for _ in range(200): # far past 1e-9 tolerance
|
|
|
|
|
+ mid = (lo + hi) / 2
|
|
|
|
|
+ if coord(x1, x2, mid) < t:
|
|
|
|
|
+ lo = mid
|
|
|
|
|
+ else:
|
|
|
|
|
+ hi = mid
|
|
|
|
|
+ return coord(y1, y2, (lo + hi) / 2)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def parse_ms(value):
|
|
|
|
|
+ m = re.fullmatch(r"(\d+(?:\.\d+)?)(ms|s)", value.strip())
|
|
|
|
|
+ if not m:
|
|
|
|
|
+ return None
|
|
|
|
|
+ return float(m.group(1)) * (1000.0 if m.group(2) == "s" else 1.0)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def check_airy_bezier():
|
|
|
|
|
+ airy = strip_comments(read("css/themes/spry-theme-airy.css"))
|
|
|
|
|
+ print("-- Airy DWM glide-in curve --")
|
|
|
|
|
+
|
|
|
|
|
+ ease = parse_bezier(token_value(airy, "--spry-dialog-ease") or "")
|
|
|
|
|
+ if not check(ease is not None, "airy --spry-dialog-ease is a cubic-bezier()",
|
|
|
|
|
+ "got: %r" % (token_value(airy, "--spry-dialog-ease"),)):
|
|
|
|
|
+ return
|
|
|
|
|
+ samples = [bezier_y(*ease, t) for t in (0.25, 0.5, 0.75)]
|
|
|
|
|
+ expected = [1 - (1 - t) ** 3 for t in (0.25, 0.5, 0.75)]
|
|
|
|
|
+ worst = max(abs(a - b) for a, b in zip(samples, expected))
|
|
|
|
|
+ check(worst < 1e-3, "airy open ease equals 1-(1-t)^3 at t=0.25/0.5/0.75",
|
|
|
|
|
+ "bezier%s sampled %s vs %s (worst delta %.2e)"
|
|
|
|
|
+ % (ease, ["%.5f" % s for s in samples],
|
|
|
|
|
+ ["%.5f" % e for e in expected], worst))
|
|
|
|
|
+
|
|
|
|
|
+ close = parse_bezier(token_value(airy, "--spry-dialog-close-ease") or "")
|
|
|
|
|
+ if not check(close is not None, "airy --spry-dialog-close-ease is a cubic-bezier()",
|
|
|
|
|
+ "got: %r" % (token_value(airy, "--spry-dialog-close-ease"),)):
|
|
|
|
|
+ return
|
|
|
|
|
+ samples = [bezier_y(*close, t) for t in (0.25, 0.5, 0.75)]
|
|
|
|
|
+ expected = [t ** 3 for t in (0.25, 0.5, 0.75)]
|
|
|
|
|
+ worst = max(abs(a - b) for a, b in zip(samples, expected))
|
|
|
|
|
+ check(worst < 1e-3, "airy close ease equals t^3 at t=0.25/0.5/0.75",
|
|
|
|
|
+ "bezier%s sampled %s vs %s (worst delta %.2e)"
|
|
|
|
|
+ % (close, ["%.5f" % s for s in samples],
|
|
|
|
|
+ ["%.5f" % e for e in expected], worst))
|
|
|
|
|
+
|
|
|
|
|
+ check(parse_ms(token_value(airy, "--spry-dialog-duration") or "") == 180.0,
|
|
|
|
|
+ "airy --spry-dialog-duration is 180ms",
|
|
|
|
|
+ "got: %r (DWM window-open T ~= 180ms)" % (token_value(airy, "--spry-dialog-duration"),))
|
|
|
|
|
+ check(parse_ms(token_value(airy, "--spry-dialog-close-duration") or "") == 150.0,
|
|
|
|
|
+ "airy --spry-dialog-close-duration is 150ms",
|
|
|
|
|
+ "got: %r" % (token_value(airy, "--spry-dialog-close-duration"),))
|
|
|
|
|
+
|
|
|
|
|
+ dia_in = token_value(airy, "--spry-dialog-in") or ""
|
|
|
|
|
+ check(re.search(r"scale\(0\.9[0-5]", dia_in) is not None,
|
|
|
|
|
+ "airy --spry-dialog-in scales from the 0.90-0.95 band",
|
|
|
|
|
+ "got: %r (DWM starts at 90-95%% of final size)" % dia_in)
|
|
|
|
|
+
|
|
|
|
|
+ # Anchored popovers additionally translate toward their trigger. The rule
|
|
|
|
|
+ # is nested inside the theme's root block (& .popover-anchor), so scan for
|
|
|
|
|
+ # its declaration list directly rather than the top-level block iterator.
|
|
|
|
|
+ anchor = re.search(r"\.popover-anchor\b[^{]*\{([^{}]*)\}", airy)
|
|
|
|
|
+ anchor_in = token_value(anchor.group(1), "--spry-dialog-in") if anchor else ""
|
|
|
|
|
+ check(anchor is not None
|
|
|
|
|
+ and re.search(r"translateY\(-", anchor_in) is not None
|
|
|
|
|
+ and re.search(r"scale\(0\.9[0-5]", anchor_in) is not None,
|
|
|
|
|
+ "airy .popover-anchor glides in from above (translateY + 0.9x scale)",
|
|
|
|
|
+ "got: %r" % anchor_in)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+# --------------------------------------------------------------------------
|
|
|
|
|
+# b. Padding defaults — bare cards and dialogs must not hug their frame
|
|
|
|
|
+# --------------------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+def check_padding_defaults():
|
|
|
|
|
+ core = strip_comments(read("css/spry.css"))
|
|
|
|
|
+ print("-- core padding defaults --")
|
|
|
|
|
+
|
|
|
|
|
+ bare_card = find_block(core, r"^:where\(\s*\.card\s*\)$")
|
|
|
|
|
+ check(bare_card is not None
|
|
|
|
|
+ and re.search(r"padding:\s*var\(--spry-card-padding\)", bare_card[1]) is not None,
|
|
|
|
|
+ "bare .card defaults to padding: var(--spry-card-padding)",
|
|
|
|
|
+ "expected a :where(.card) rule padding bare cards")
|
|
|
|
|
+
|
|
|
|
|
+ guard_card = find_block(core, r":where\(\.card:has\(")
|
|
|
|
|
+ guard_sel = collapse_ws(guard_card[0]) if guard_card else ""
|
|
|
|
|
+ check(guard_card is not None
|
|
|
|
|
+ and re.search(r"padding:\s*0", guard_card[1]) is not None
|
|
|
|
|
+ and all(c in guard_sel for c in (".card-header", ".card-body", ".card-footer")),
|
|
|
|
|
+ "structured .card (has header/body/footer) zeroes the default padding",
|
|
|
|
|
+ "got selector: %r" % guard_sel)
|
|
|
|
|
+
|
|
|
|
|
+ root_block = find_block(core, r":where\(:root\)")
|
|
|
|
|
+ check(root_block is not None
|
|
|
|
|
+ and token_value(root_block[1], "--spry-popover-padding") is not None,
|
|
|
|
|
+ "--spry-popover-padding is defined in :where(:root)")
|
|
|
|
|
+ check(token_value(core, "--spry-card-padding") is not None,
|
|
|
|
|
+ "--spry-card-padding is defined")
|
|
|
|
|
+
|
|
|
|
|
+ popover = find_block(core, r"^\[popover\]$")
|
|
|
|
|
+ check(popover is not None
|
|
|
|
|
+ and re.search(r"padding:\s*var\(--spry-popover-padding\)", popover[1]) is not None,
|
|
|
|
|
+ "[popover] pads with var(--spry-popover-padding)")
|
|
|
|
|
+
|
|
|
|
|
+ popover_win = find_block(core, r"\[popover\]:has\(")
|
|
|
|
|
+ win_sel = collapse_ws(popover_win[0]) if popover_win else ""
|
|
|
|
|
+ check(popover_win is not None
|
|
|
|
|
+ and re.search(r"padding:\s*0", popover_win[1]) is not None
|
|
|
|
|
+ and all(c in win_sel for c in (".dialog-titlebar", ".dialog-body", ".dialog-footer")),
|
|
|
|
|
+ "popover 'windows' (has dialog parts) drop the generic padding",
|
|
|
|
|
+ "got selector: %r" % win_sel)
|
|
|
|
|
+
|
|
|
|
|
+ bare_dialog = find_block(core, r"dialog:not\(:has\(")
|
|
|
|
|
+ dsel = collapse_ws(bare_dialog[0]) if bare_dialog else ""
|
|
|
|
|
+ check(bare_dialog is not None
|
|
|
|
|
+ and re.search(r"padding:\s*var\(--spry-popover-padding\)", bare_dialog[1]) is not None
|
|
|
|
|
+ and all(c in dsel for c in (".dialog-titlebar", ".dialog-body",
|
|
|
|
|
+ ".dialog-footer", ".card-header",
|
|
|
|
|
+ ".card-body", ".card-footer")),
|
|
|
|
|
+ "bare dialog:not(:has(structural parts)) pads itself",
|
|
|
|
|
+ "got selector: %r" % dsel)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+# --------------------------------------------------------------------------
|
|
|
|
|
+# c. Theme token sets
|
|
|
|
|
+# --------------------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+def check_theme_tokens():
|
|
|
|
|
+ print("-- theme token sets --")
|
|
|
|
|
+ for slug in THEMES:
|
|
|
|
|
+ css = strip_comments(read("css/themes/spry-theme-%s.css" % slug))
|
|
|
|
|
+ check(token_value(css, "--spry-card-padding") is not None,
|
|
|
|
|
+ "theme %s defines --spry-card-padding" % slug)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+# --------------------------------------------------------------------------
|
|
|
|
|
+# d. Demo page coverage
|
|
|
|
|
+# --------------------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+def check_demo():
|
|
|
|
|
+ html = read("css/index.html")
|
|
|
|
|
+ print("-- demo page coverage --")
|
|
|
|
|
+
|
|
|
|
|
+ missing = ["spry.css"] + ["themes/spry-theme-%s.css" % s for s in THEMES]
|
|
|
|
|
+ missing = [href for href in missing
|
|
|
|
|
+ if ('href="%s"' % href) not in html and ("href='%s'" % href) not in html]
|
|
|
|
|
+ check(not missing, "demo links spry.css and all 7 themes",
|
|
|
|
|
+ "missing links: %s" % ", ".join(missing))
|
|
|
|
|
+
|
|
|
|
|
+ inputs = re.findall(r"<input\b[^>]*>", html)
|
|
|
|
|
+ missing = [slug for slug in THEMES
|
|
|
|
|
+ if not any('type="radio"' in tag and ('id="spry-theme-%s"' % slug) in tag
|
|
|
|
|
+ for tag in inputs)]
|
|
|
|
|
+ check(not missing, "demo has a theme radio for every slug",
|
|
|
|
|
+ "missing radios: %s" % ", ".join(missing))
|
|
|
|
|
+
|
|
|
|
|
+ missing = []
|
|
|
|
|
+ for marker in DEMO_MARKERS:
|
|
|
|
|
+ if not re.search(r"\b%s\b" % re.escape(marker), html):
|
|
|
|
|
+ missing.append(marker)
|
|
|
|
|
+ classes = re.findall(r'class="([^"]*)"', html)
|
|
|
|
|
+ if not any("prose" in c.split() for c in classes):
|
|
|
|
|
+ missing.append("prose (as a class)")
|
|
|
|
|
+ check(not missing, "demo exercises the key components (dialogs, drawers, prose, ...)",
|
|
|
|
|
+ "missing markers: %s" % ", ".join(missing))
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+# --------------------------------------------------------------------------
|
|
|
|
|
+# e. Formal prose guard — both activation routes
|
|
|
|
|
+# --------------------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+def check_formal_prose():
|
|
|
|
|
+ formal = strip_comments(read("css/themes/spry-theme-formal.css"))
|
|
|
|
|
+ print("-- formal .prose guard --")
|
|
|
|
|
+
|
|
|
|
|
+ block = find_block(formal, r'\[data-theme="formal"\]')
|
|
|
|
|
+ sel = collapse_ws(block[0]) if block else ""
|
|
|
|
|
+ check(block is not None and "html:has(#spry-theme-formal:checked)" in sel,
|
|
|
|
|
+ "formal rules are reachable via data-theme AND the demo radio route",
|
|
|
|
|
+ "got selector: %r" % sel)
|
|
|
|
|
+
|
|
|
|
|
+ body = block[1] if block else ""
|
|
|
|
|
+ check("h1:not(.prose" in body and "h2:not(.prose" in body,
|
|
|
|
|
+ "formal h1/h2 size rules opt out of .prose")
|
|
|
|
|
+
|
|
|
|
|
+ # Scan every h1/h2 rule: if its own declarations set font-size, the
|
|
|
|
|
+ # selector must carry the :not(.prose *) guard.
|
|
|
|
|
+ unguarded = []
|
|
|
|
|
+ for m in re.finditer(r"\bh([12])\b", formal):
|
|
|
|
|
+ nxt = re.search(r"[{};]", formal[m.end():])
|
|
|
|
|
+ if not nxt or nxt.group(0) != "{":
|
|
|
|
|
+ continue # not in a selector position
|
|
|
|
|
+ brace = m.end() + nxt.start()
|
|
|
|
|
+ prev = max(formal.rfind("}", 0, brace), formal.rfind("{", 0, brace),
|
|
|
|
|
+ formal.rfind(";", 0, brace))
|
|
|
|
|
+ selector = collapse_ws(formal[prev + 1:brace])
|
|
|
|
|
+ if not re.search(r"\bh[12]\b", selector):
|
|
|
|
|
+ continue # the h1/h2 belonged to an enclosing context, not this selector
|
|
|
|
|
+ decls_end = re.search(r"[{}]", formal[brace + 1:])
|
|
|
|
|
+ decls = formal[brace + 1: brace + 1 + decls_end.start()] if decls_end else ""
|
|
|
|
|
+ if "font-size" in decls and ":not(.prose" not in selector:
|
|
|
|
|
+ unguarded.append(selector)
|
|
|
|
|
+ check(not unguarded, "every formal h1/h2 font-size rule is guarded by :not(.prose ...)",
|
|
|
|
|
+ "unguarded: %s" % " | ".join(unguarded))
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+# --------------------------------------------------------------------------
|
|
|
|
|
+# f. Syntax sanity
|
|
|
|
|
+# --------------------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+def check_syntax():
|
|
|
|
|
+ print("-- syntax sanity --")
|
|
|
|
|
+ files = ["css/spry.css"] + ["css/themes/spry-theme-%s.css" % s for s in THEMES]
|
|
|
|
|
+ for rel in files:
|
|
|
|
|
+ raw = read(rel)
|
|
|
|
|
+ stripped = strip_comments(raw)
|
|
|
|
|
+ problems = []
|
|
|
|
|
+ if stripped.count("{") != stripped.count("}"):
|
|
|
|
|
+ problems.append("braces %d/%d" % (stripped.count("{"), stripped.count("}")))
|
|
|
|
|
+ if stripped.count("(") != stripped.count(")"):
|
|
|
|
|
+ problems.append("parens %d/%d" % (stripped.count("("), stripped.count(")")))
|
|
|
|
|
+ if "\t" in raw:
|
|
|
|
|
+ problems.append("tab characters (files are 2-space)")
|
|
|
|
|
+ check(not problems, "%s is balanced and tab-free" % rel,
|
|
|
|
|
+ "; ".join(problems))
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+# --------------------------------------------------------------------------
|
|
|
|
|
+# g. README freshness
|
|
|
|
|
+# --------------------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+def check_readme():
|
|
|
|
|
+ print("-- README freshness --")
|
|
|
|
|
+ readme = read("README.md")
|
|
|
|
|
+ check("../Spry-CSS" not in readme, "README has no stale ../Spry-CSS references")
|
|
|
|
|
+ check("css/index.html" in readme, "README points at the css/index.html demo")
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def main():
|
|
|
|
|
+ for fn in (check_airy_bezier, check_padding_defaults, check_theme_tokens,
|
|
|
|
|
+ check_demo, check_formal_prose, check_syntax, check_readme):
|
|
|
|
|
+ try:
|
|
|
|
|
+ fn()
|
|
|
|
|
+ except Exception as exc: # a crash is a failure, not a skip
|
|
|
|
|
+ check(False, "%s ran to completion" % fn.__name__,
|
|
|
|
|
+ "raised: %r" % exc)
|
|
|
|
|
+ print("---- %d passed, %d failed ----" % (passes, failures))
|
|
|
|
|
+ return 1 if failures else 0
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+if __name__ == "__main__":
|
|
|
|
|
+ sys.exit(main())
|