Skip to content

Commit 68f692b

Browse files
authored
test(canvas): cover unicode-escape decoding, guard JSXText write-back
Lift the decoder out of the bootstrap string as an exported function (interpolated back in via toString) so it can be unit-tested, add parameterised tests over the decode cases, and only write JSXText values back when they actually changed. Addresses Greptile review. Generated-By: PostHog Code Task-Id: 20fa55b3-faa2-41b2-91a4-a6bab80ec174
1 parent c8f1963 commit 68f692b

2 files changed

Lines changed: 79 additions & 12 deletions

File tree

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { describe, expect, it } from "vitest";
2+
import {
3+
buildSandboxDocument,
4+
decodeJsxUnicodeEscapes,
5+
} from "./sandboxRuntime";
6+
7+
describe("decodeJsxUnicodeEscapes", () => {
8+
it.each([
9+
{
10+
name: "decodes 4-hex escapes",
11+
input: "Survey started Jun 20, 2026 \\u00b7 live \\u00b7 data",
12+
expected: "Survey started Jun 20, 2026 · live · data",
13+
},
14+
{ name: "decodes braced code points", input: "\\u{1F600}", expected: "😀" },
15+
{
16+
name: "decodes surrogate pairs",
17+
input: "\\ud83d\\ude00",
18+
expected: "😀",
19+
},
20+
{
21+
name: "decodes braced escapes shorter than 4 digits",
22+
input: "\\u{b7}",
23+
expected: "·",
24+
},
25+
{
26+
name: "leaves out-of-range code points intact",
27+
input: "\\u{110000}",
28+
expected: "\\u{110000}",
29+
},
30+
{
31+
name: "leaves incomplete escapes intact",
32+
input: "\\u00 and \\uZZZZ",
33+
expected: "\\u00 and \\uZZZZ",
34+
},
35+
{
36+
name: "leaves already-decoded text untouched",
37+
input: "plain · text",
38+
expected: "plain · text",
39+
},
40+
{
41+
name: "decodes valid escapes next to invalid ones",
42+
input: "\\u00b7 then \\u{110000}",
43+
expected: "· then \\u{110000}",
44+
},
45+
])("$name", ({ input, expected }) => {
46+
expect(decodeJsxUnicodeEscapes(input)).toBe(expected);
47+
});
48+
});
49+
50+
describe("buildSandboxDocument", () => {
51+
it("inlines the unicode-escape decoder into the bootstrap", () => {
52+
const html = buildSandboxDocument("edit");
53+
expect(html).toContain(
54+
"const decodeUnicodeEscapes = function decodeJsxUnicodeEscapes(",
55+
);
56+
expect(html).toContain("jsxUnicodeEscapesPlugin");
57+
});
58+
});

packages/ui/src/features/canvas/freeform/sandboxRuntime.ts

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,22 @@ const TAILWIND_V3 = `<script src="https://cdn.tailwindcss.com"></script>
127127
} } };
128128
</script>`;
129129

130+
// Decodes literal \uXXXX / \u{...} escape sequences in a string. Exported for
131+
// tests; its source is interpolated into the sandbox bootstrap below so the
132+
// iframe runs this exact implementation.
133+
export function decodeJsxUnicodeEscapes(value: string): string {
134+
return value.replace(
135+
/\\u\{([0-9a-fA-F]{1,6})\}|\\u([0-9a-fA-F]{4})/g,
136+
(match, braced, plain) => {
137+
try {
138+
return String.fromCodePoint(Number.parseInt(braced || plain, 16));
139+
} catch {
140+
return match;
141+
}
142+
},
143+
);
144+
}
145+
130146
export function buildSandboxDocument(
131147
mode: SandboxMode,
132148
// The PostHog host, when in-iframe analytics/replay is enabled. Opens CSP for
@@ -274,21 +290,14 @@ export function buildSandboxDocument(
274290
// contain them despite the prompt rules — decode at transpile time so both
275291
// new and already-saved canvases render the real characters. Escapes inside
276292
// JS string/template literals are untouched (Babel already decoded those).
277-
const decodeUnicodeEscapes = (value) =>
278-
value.replace(
279-
/\\\\u\\{([0-9a-fA-F]{1,6})\\}|\\\\u([0-9a-fA-F]{4})/g,
280-
(match, braced, plain) => {
281-
try {
282-
return String.fromCodePoint(parseInt(braced || plain, 16));
283-
} catch {
284-
return match;
285-
}
286-
},
287-
);
293+
const decodeUnicodeEscapes = ${decodeJsxUnicodeEscapes.toString()};
288294
const jsxUnicodeEscapesPlugin = () => ({
289295
visitor: {
290296
JSXText(path) {
291-
path.node.value = decodeUnicodeEscapes(path.node.value);
297+
const decoded = decodeUnicodeEscapes(path.node.value);
298+
if (decoded !== path.node.value) {
299+
path.node.value = decoded;
300+
}
292301
},
293302
JSXAttribute(path) {
294303
const v = path.node.value;

0 commit comments

Comments
 (0)