distinct / scripts /cascade_audit.py
User1342's picture
Harden every component declaration against Gradio's reset; denser library grid
9535829
Raw History Blame Contribute Delete
5.37 kB
"""Does the stylesheet the browser computed match the stylesheet we wrote?
This exists because the same bug shipped twice. Gradio's reset is scoped to a
version-stamped container class, so rules like ``.gradio-container-6-24-0 p``
and ``.gradio-container-6-24-0 [type="radio"]`` outrank a plain component
selector. Worse, the sheets are injected in a different order on Spaces than in
local dev, so a rule can win on a laptop and lose in production -- which is how
a redesign reached the Space looking, in the reviewer's words, identical.
:mod:`distinct_server.presentation` now hardens every ``.c-`` declaration at
import time, and ``tests/test_stylesheet_hardening.py`` proves that pass is
lossless. Neither can prove the result survived *this* browser and *this*
injection order. That is what this does, and why it is run against production
after every push rather than only against a local server.
Run it directly::
python scripts/cascade_audit.py http://127.0.0.1:7860
A finding is only reported when the value that won cannot be produced by any
``.c-`` rule matching the element. A base rule beaten by one of our own variant
or state rules (``.c-chip--outline``, ``.c-libcard.is-on``, the mobile
``@media`` block) is ordinary cascade and is filtered out.
"""
from __future__ import annotations
import json
import sys
# Properties whose computed form legitimately differs from the declared one, or
# where a mismatch says nothing about the cascade.
_SKIP = [
"content", "transition", "animation", "cursor", "pointer-events",
"user-select", "will-change", "contain", "isolation", "background",
"background-image", "transform", "filter", "box-shadow", "clip",
"clip-path", "aspect-ratio", "flex", "inset", "all", "font-family",
]
AUDIT_JS = """() => {
const SKIP=new Set(%s);
const bad=[];let n=0;
for (const sheet of document.styleSheets){ let rules;try{rules=sheet.cssRules}catch(e){continue}
for (const rule of rules){
if(!rule.selectorText||!rule.style||!rule.selectorText.includes('.c-'))continue;
let els;try{els=document.querySelectorAll(rule.selectorText)}catch(e){continue}
for(const p of rule.style){
if(SKIP.has(p)||p.startsWith('--')||p.startsWith('-webkit'))continue;
const want=rule.style.getPropertyValue(p).trim(); if(!want)continue;
for(const el of els){ n++;
// The intended value is read in the element's OWN box and font context by
// forcing the declaration inline: an em-based line-height or a ch-based
// max-width computes to a different number on a probe element, and
// comparing against that number invents findings that are not there.
const got=getComputedStyle(el).getPropertyValue(p);
const prev=el.style.getPropertyValue(p),pri=el.style.getPropertyPriority(p);
el.style.setProperty(p,want,'important');
const intended=getComputedStyle(el).getPropertyValue(p);
if(prev)el.style.setProperty(p,prev,pri);else el.style.removeProperty(p);
if(got!==intended)bad.push({el,sel:rule.selectorText.slice(0,80),prop:p,want:intended,got});
}}}}
const real=bad.filter(b=>{
const el=b.el; let ours=false;
for(const sheet of document.styleSheets){ let rules;try{rules=sheet.cssRules}catch(e){continue}
for(const r of rules){ if(!r.selectorText||!r.style)continue;
if(!r.selectorText.includes('.c-'))continue;
let m=false;try{m=el.matches(r.selectorText)}catch(e){}
if(!m)continue;
// Apply the whole rule rather than one longhand: a variant usually writes
// the shorthand (border, margin), and asking it for `border-top-style`
// or `margin-top` on its own can come back empty.
const keep=el.getAttribute('style');
el.setAttribute('style',(keep||'')+';'+r.style.cssText.replace(/;/g,' !important;'));
const c=getComputedStyle(el).getPropertyValue(b.prop);
if(keep===null)el.removeAttribute('style');else el.setAttribute('style',keep);
if(c===b.got){ours=true;break;}
} if(ours)break; }
return !ours;
});
const u={};real.forEach(b=>u[b.sel+'|'+b.prop]={sel:b.sel,prop:b.prop,want:b.want,got:b.got});
return {checked:n,differ:bad.length,overridden:Object.keys(u).length,
list:Object.values(u).slice(0,40)};
}""" % json.dumps(_SKIP)
def audit(page) -> dict:
"""Run the audit on an open Playwright page and return the report."""
return page.evaluate(AUDIT_JS)
def describe(report: dict) -> str:
line = (
f"{report['checked']} declarations checked, {report['differ']} differ from "
f"the rule that declared them, {report['overridden']} decided from outside "
f"our sheet"
)
if not report["overridden"]:
return line
rows = "\n".join(
f" {row['sel']} :: {row['prop']} wanted {row['want']}, got {row['got']}"
for row in report["list"]
)
return line + "\n" + rows
def main(url: str) -> int:
from playwright.sync_api import sync_playwright
with sync_playwright() as pw:
browser = pw.chromium.launch()
page = browser.new_page(viewport={"width": 1280, "height": 900})
page.goto(url, wait_until="load")
page.wait_for_timeout(2000)
report = audit(page)
browser.close()
print(describe(report))
return 1 if report["overridden"] else 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1] if len(sys.argv) > 1 else "http://127.0.0.1:7860"))