Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

facett-ops/tests/no_js_graphics.rs

//! **RED FIRST for the no-JavaScript-graphics rule.**
//!
//! Rickard's rule for this whole lane is one sentence — *"it must NOT use javascript
//! to do graphics, it must REUSE the RUST components, packaged in wasm"* — and a
//! sentence in a briefing enforces nothing. `facett_ops::jsguard` is the mechanical form,
//! and `pip::stage_package` runs it on the loader page before a wheel can be built.
//!
//! Every test below hands the scanner a page that DRAWS and asserts it is caught by
//! name. The most important one is `the_real_loader_page_passes`, paired with
//! `the_probe_exception_is_narrow`: the shipped page legitimately calls
//! `getContext`, inside the read-back probe that proves the renderer is WebGPU. A
//! guard that banned the substring would delete that proof and call itself green.

use facett_ops::jsguard;

/// The shipped loader page, read from the package source. Not a copy: a copy would
/// drift, and then this suite would be guarding a page nobody ships.
fn real_loader() -> String {
    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("..")
        .join("pkg-facet-wrapped/src/facett/assets/index.html");
    std::fs::read_to_string(&path)
        .unwrap_or_else(|e| panic!("the shipped loader must exist at {}: {e}", path.display()))
}

// ── the GREEN case: what we actually ship ──────────────────────────────────

#[test]
fn the_real_loader_page_passes() {
    let html = real_loader();
    let findings = jsguard::scan(&html);
    assert!(
        findings.is_empty(),
        "the shipped loader must not draw, but the guard found:\n{}",
        findings.iter().map(|f| f.to_string()).collect::<Vec<_>>().join("\n")
    );
    assert!(jsguard::check("loader", &html).is_ok());
}

#[test]
fn the_shipped_loader_really_does_contain_the_thing_the_exception_is_for() {
    // If this ever fails, the exception below is guarding nothing and must go. A
    // carve-out with no remaining reason is how a guard rots into a loophole.
    let html = real_loader();
    assert!(
        html.contains("window.facet_canvas_api"),
        "the read-back probe is gone — delete the getContext exception with it"
    );
    assert!(
        html.contains("getContext(\"webgpu\")"),
        "the probe no longer asks the canvas which API drew; that is the only \
         independent evidence we have that this is WebGPU and not WebGL"
    );
}

// ── RED: JavaScript painting on a canvas ───────────────────────────────────

#[test]
fn a_canvas_painter_is_refused_verb_by_verb() {
    // Each verb on its own: a guard that only catches `fillRect` is a guard that
    // catches the one example someone tested it with.
    for verb in
        ["fillRect", "strokeRect", "beginPath", "lineTo", "fillText", "drawImage", "putImageData"]
    {
        let html = format!("<script>function draw(ctx) {{ ctx.{verb}(0,0,10,10); }}</script>");
        let findings = jsguard::scan(&html);
        assert_eq!(findings.len(), 1, "`{verb}` must be caught exactly once");
        assert_eq!(findings[0].construct, verb);
        assert!(jsguard::check("page", &html).is_err(), "`{verb}` must refuse the build");
    }
}

// ── RED: the knut defect — a hand-built SVG renderer ───────────────────────

#[test]
fn a_hand_built_svg_renderer_is_refused() {
    // This is the exact shape of `knut-pipelines/src/web.rs:293-405`: JS building
    // SVG nodes for a DAG that the facett `pipeline` component already renders.
    let html = r#"<script>
      const n = document.createElementNS("http://www.w3.org/2000/svg", "rect");
      svg.appendChild(n);
    </script>"#;
    let findings = jsguard::scan(html);
    assert!(findings.iter().any(|f| f.construct == "createElementNS"));
    assert!(jsguard::check("dag", html).is_err());
}

#[test]
fn an_inline_svg_element_is_refused() {
    let html = "<div><svg width=\"400\" height=\"300\"></svg></div>";
    let findings = jsguard::scan(html);
    assert_eq!(findings.len(), 1);
    assert_eq!(findings[0].construct, "<svg");
}

// ── RED: a charting library, which is a component by proxy ─────────────────

#[test]
fn a_charting_library_is_refused_by_name() {
    // `py/panes/population_pane.py` renders through Plotly today — 165 lines of
    // pandas + plotly that draw in the browser's JS. Same banned class, and this is
    // the guard that will refuse it once that pane's page is the one being staged.
    for lib in ["plotly", "echarts", "chart.js", "vega-lite"] {
        let html = format!("<script src=\"https://cdn.example/{lib}\"></script>");
        let findings = jsguard::scan(&html);
        assert!(
            findings.iter().any(|f| f.construct.eq_ignore_ascii_case(lib)),
            "`{lib}` must be refused by name"
        );
    }
}

// ── RED: the exception is NARROW ───────────────────────────────────────────

#[test]
fn the_probe_exception_is_narrow() {
    // Inside the probe: allowed, because it is asking which API drew.
    let inside = r#"
      window.facet_canvas_api = () => {
        try { if (c.getContext("webgpu")) return "webgpu"; } catch (_) {}
        return "none";
      };
    "#;
    assert!(jsguard::scan(inside).is_empty(), "the read-back probe must stay legal");

    // The same call, one function over: refused. This is a renderer acquiring a
    // surface, and it is how JS graphics would come back in.
    let outside = r#"
      function render() {
        const ctx = document.getElementById("c").getContext("2d");
        ctx.fill();
      }
    "#;
    let findings = jsguard::scan(outside);
    assert!(
        findings.iter().any(|f| f.construct == "getContext"),
        "getContext outside the probe must be refused, got {findings:?}"
    );
}

#[test]
fn the_probe_exception_closes_at_the_end_of_the_probe() {
    // A page that opens the probe and then keeps going must NOT get a blanket pass
    // for the rest of the file. If the region never closed, everything below one
    // `window.facet_canvas_api` line would be exempt — which is exactly how a
    // narrow exception becomes a hole.
    let html = r#"
      window.facet_canvas_api = () => {
        try { if (c.getContext("webgpu")) return "webgpu"; } catch (_) {}
      };
      function sneak() {
        const ctx = canvas.getContext("2d");
      }
    "#;
    let findings = jsguard::scan(html);
    assert_eq!(
        findings.iter().filter(|f| f.construct == "getContext").count(),
        1,
        "exactly the one AFTER the probe must be caught, got {findings:?}"
    );
    assert!(findings[0].text.contains("2d"));
}

// ── the refusal has to be usable ───────────────────────────────────────────

#[test]
fn the_refusal_names_the_line_and_the_reason() {
    let html = "<html>\n<body>\n<script>ctx.fillRect(0,0,1,1);</script>\n</body>\n</html>";
    let err = jsguard::check("assets/index.html", html).unwrap_err();
    assert!(err.contains("assets/index.html"), "must name the file: {err}");
    assert!(err.contains("line 3"), "must name the line: {err}");
    assert!(err.contains("fillRect"), "must name the construct: {err}");
    assert!(err.contains("wasm"), "must say why: {err}");
}