"""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"))