Files
NexusOS/interface/web/src/preview/jsx-transform.test.js
T
AthenaandCursor 656c14caf3 feat(preview): add sandboxed live code previews
Render validated HTML, SVG, JSX, and TSX fences locally while preserving tool context and preventing explanatory JSON from triggering actions.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-26 03:34:19 -05:00

252 lines
11 KiB
JavaScript

/*
* node --test src/preview/jsx-transform.test.js
*
* The transform runs on model-authored text, so these cases are the shapes a
* model actually emits, plus the ones where a naive scanner silently produces
* code that runs and does the wrong thing (generics read as comparisons, `<` in
* a string read as a tag). Silent-wrong is the failure mode worth testing;
* anything that throws is already visible to the user.
*/
import { test } from "node:test";
import assert from "node:assert/strict";
import { transform, TransformError } from "./jsx-transform.js";
// Whitespace is normalised for comparison: the transform drops the space a
// stripped annotation sat in (`const x: T = 1` -> `const x= 1`), which is
// invisible to everyone because the Code tab shows the original source, not
// this output.
const js = (src) => transform(src).code.replace(/\s+/g, " ").trim();
// ── elements ────────────────────────────────────────────────────────────────
test("element with no attributes or children", () => {
assert.equal(js("const a = <div />;"), 'const a = h("div",null);');
});
test("lowercase tags become strings, capitalized stay identifiers", () => {
assert.equal(js("<div />"), 'h("div",null)');
assert.equal(js("<App />"), "h(App,null)");
assert.equal(js("<Foo.Bar />"), "h(Foo.Bar,null)");
});
test("string, expression, boolean and hyphenated attributes", () => {
assert.equal(js('<a href="/x" />'), 'h("a",{href:"/x"})');
assert.equal(js("<a n={1 + 2} />"), 'h("a",{n:(1 + 2)})');
assert.equal(js("<input disabled />"), 'h("input",{disabled:true})');
assert.equal(js('<a data-id="7" />'), 'h("a",{"data-id":"7"})');
});
test("attribute spread", () => {
assert.equal(js("<div {...props} id=\"x\" />"), 'h("div",{...(props),id:"x"})');
});
test("children: text, expressions and nesting", () => {
assert.equal(js("<p>hi</p>"), 'h("p",null,"hi")');
assert.equal(js("<p>{name}</p>"), 'h("p",null,(name))');
assert.equal(js("<p>a {b} c</p>"), 'h("p",null,"a ",(b)," c")');
assert.equal(js("<ul><li>x</li></ul>"), 'h("ul",null,h("li",null,"x"))');
});
test("fragments", () => {
assert.equal(js("<><a /><b /></>"), 'h(Fragment,null,h("a",null),h("b",null))');
});
test("JSX nested inside an expression child", () => {
assert.equal(
js("<ul>{items.map((i) => <li key={i}>{i}</li>)}</ul>"),
'h("ul",null,(items.map((i) => h("li",{key:(i)},(i)))))',
);
});
test("JSX comments render nothing", () => {
assert.equal(js("<div>{/* note */}<a /></div>"), 'h("div",null,h("a",null))');
});
test("whitespace-only lines between elements collapse away", () => {
assert.equal(
js("<div>\n <a />\n <b />\n</div>"),
'h("div",null,h("a",null),h("b",null))',
);
});
test("text spanning lines keeps single spaces", () => {
assert.equal(js("<p>\n one\n two\n</p>"), 'h("p",null,"one two")');
});
// ── things that must NOT be treated as JSX ──────────────────────────────────
test("comparisons and arrow bodies are left alone", () => {
assert.equal(js("const t = a < b;"), "const t = a < b;");
assert.equal(js("if (x < 3 && y > 1) {}"), "if (x < 3 && y > 1) {}");
assert.equal(js("const f = (a, b) => a < b;"), "const f = (a, b) => a < b;");
});
test("angle brackets inside strings, templates and regexes survive", () => {
assert.equal(js('const s = "<div>not jsx</div>";'), 'const s = "<div>not jsx</div>";');
assert.equal(js("const s = `a <b> c`;"), "const s = `a <b> c`;");
assert.equal(js("const r = /<[a-z]+>/g;"), "const r = /<[a-z]+>/g;");
});
test("template interpolation can still contain JSX", () => {
assert.equal(js("const s = `${<a />}`;"), "const s = `${h(\"a\",null)}`;");
});
test("division is not mistaken for a regex", () => {
assert.equal(js("const r = (a + b) / 2 / c;"), "const r = (a + b) / 2 / c;");
// A `/` right after a call's closing paren: `)` ends a value, so this is
// division. Treating it as a regex swallowed the rest of the line.
assert.equal(js("const y = Math.sin((i + s) / 6) * 70;"), "const y = Math.sin((i + s) / 6) * 70;");
assert.equal(js("const m = xs[0] / total;"), "const m = xs[0] / total;");
});
test("indexing and calls before < are comparisons, not JSX", () => {
assert.equal(js("if (xs[0] < 3) {}"), "if (xs[0] < 3) {}");
assert.equal(js("while (f(i) < n) { i++; }"), "while (f(i) < n) { i++; }");
});
// ── TypeScript ──────────────────────────────────────────────────────────────
test("parameter and variable annotations are stripped", () => {
assert.equal(js("function f(a: string, b: number) {}"), "function f(a, b) {}");
assert.equal(js("const x: number = 5;"), "const x= 5;");
assert.equal(js("const f = (n: number): string => String(n);"), "const f = (n)=> String(n);");
});
test("interface and type declarations are dropped whole", () => {
assert.equal(js("interface Props { a: string; b?: number }\nconst x = 1;"), "const x = 1;");
assert.equal(js("type Id = string | number;\nconst x = 1;"), "const x = 1;");
});
test("generic call arguments are stripped, not read as comparisons", () => {
// The silent-wrong case: `useState<number>(0)` is valid JS meaning
// `(useState < number) > (0)`, so getting this wrong yields a boolean.
assert.equal(js("const [n, setN] = useState<number>(0);"), "const [n, setN] = useState(0);");
assert.equal(js("useRef<HTMLCanvasElement | null>(null);"), "useRef(null);");
});
test("as-casts and non-null assertions are stripped", () => {
assert.equal(js("const el = x as HTMLElement;"), "const el = x ;");
assert.equal(js("const v = raw as const;"), "const v = raw ;");
assert.equal(js("ref.current!.focus();"), "ref.current.focus();");
});
test("optional parameters keep their default values", () => {
assert.equal(js("function f(a: number = 3) { return a; }"), "function f(a= 3) { return a; }");
});
test("object literals and ternaries are not mistaken for annotations", () => {
assert.equal(js("const o = { a: 1, b: 'two' };"), "const o = { a: 1, b: 'two' };");
assert.equal(js("const v = c ? 1 : 2;"), "const v = c ? 1 : 2;");
assert.equal(js("el.style = { color: 'red' };"), "el.style = { color: 'red' };");
// Object literals passed as arguments sit inside a parameter list, where
// parameter annotations also live.
assert.equal(js("f({ a: 1, b: 2 });"), "f({ a: 1, b: 2 });");
assert.equal(js("ctx.fillRect(x, y, { w: 1 });"), "ctx.fillRect(x, y, { w: 1 });");
});
test("ternaries inside a call keep their else-branch", () => {
// Both of these end in `}` or `)` before the `:`, exactly like a destructured
// parameter annotation and a return type - only the pending `?` tells them apart.
assert.equal(js("f(cond ? { a: 1 } : { b: 2 });"), "f(cond ? { a: 1 } : { b: 2 });");
assert.equal(js("f(cond ? g() : h2());"), "f(cond ? g() : h2());");
assert.equal(js("const s = ok ? 'y' : 'n';"), "const s = ok ? 'y' : 'n';");
});
test("destructured parameter annotations are stripped", () => {
assert.equal(js("function C({ start }: Props) {}"), "function C({ start }) {}");
assert.equal(js("const f = ({ a, b }: P) => a + b;"), "const f = ({ a, b }) => a + b;");
assert.equal(js("function g([x, y]: Pair) {}"), "function g([x, y]) {}");
});
// ── modules ─────────────────────────────────────────────────────────────────
test("imports are dropped - the sandbox has no module loader", () => {
assert.equal(js('import React, { useState } from "react";\nconst x = 1;'), "const x = 1;");
assert.equal(js('import "./styles.css";\nconst x = 1;'), "const x = 1;");
});
test("dropped imports report the bindings they would have provided", () => {
// So a missing name can say where it was supposed to come from, instead of
// surfacing as "useInView is not defined" at its first use.
const { imports } = transform(
'import React, { useState, useEffect } from "react";\n' +
'import { useInView } from "react-infinite-scroll";\n' +
'import * as d3 from "d3";\nconst x = 1;',
);
assert.deepEqual(imports, [
{ names: ["React", "useState", "useEffect"], module: "react" },
{ names: ["useInView"], module: "react-infinite-scroll" },
{ names: ["d3"], module: "d3" },
]);
});
test("a malformed import still yields its name and module", () => {
// Straight from a transcript: a missing closing brace. The import is dropped
// either way, so the binding it meant to create is what matters.
const { imports } = transform("import { useInView from 'react-infinite-scroll';\nconst x = 1;");
assert.deepEqual(imports, [{ names: ["useInView"], module: "react-infinite-scroll" }]);
});
test("export default names the mount target", () => {
assert.equal(transform("export default function App() {}").defaultExport, "App");
assert.equal(transform("function A() {}\nexport default A;").defaultExport, "A");
assert.equal(js("export default function App() {}"), "function App() {}");
});
test("named exports are unwrapped", () => {
assert.equal(js("export const x = 1;"), "const x = 1;");
assert.equal(js("export function Chart() {}"), "function Chart() {}");
});
// ── failure is loud ─────────────────────────────────────────────────────────
test("mismatched closing tag throws with a line number", () => {
assert.throws(() => transform("<div>\n<span>x</div>"), (e) =>
e instanceof TransformError && /does not match/.test(e.message) && /line 2/.test(e.message));
});
test("unterminated element throws", () => {
assert.throws(() => transform("const a = <div>"), TransformError);
});
// ── a whole component, end to end ───────────────────────────────────────────
test("a realistic component transforms to runnable JS", () => {
const src = `
import { useState } from "react";
interface Props { start: number }
export default function Counter({ start }: Props) {
const [n, setN] = useState<number>(start);
return (
<div className="box">
<button onClick={() => setN(n + 1)}>+1</button>
<span>{n} clicks</span>
{n > 3 && <em>many!</em>}
</div>
);
}`;
const out = transform(src);
assert.equal(out.defaultExport, "Counter");
assert.match(out.code, /function Counter\(\{ start \}\)/);
assert.match(out.code, /useState\(start\)/);
assert.match(out.code, /h\("button",\{onClick:\(\(\) => setN\(n \+ 1\)\)\},"\+1"\)/);
assert.doesNotMatch(out.code, /interface|import|: Props|<number>/);
// The real proof: it parses as JS.
assert.doesNotThrow(() => new Function(out.code));
});
test("output of every element case parses as JS", () => {
for (const src of [
"<div />",
"<a href=\"/x\">link</a>",
"<><p>a</p><p>b</p></>",
"const v = <ul>{xs.map((x) => <li key={x}>{x}</li>)}</ul>;",
"const v = <Foo {...p} n={1} on={() => f(`${x}`)} />;",
]) {
const { code } = transform(src);
assert.doesNotThrow(() => new Function("h", "Fragment", "xs", "p", "x", "f", code), src);
}
});