127 lines
3.7 KiB
TypeScript
127 lines
3.7 KiB
TypeScript
import { render } from "@testing-library/react";
|
|
import { describe, expect, it } from "vitest";
|
|
import { SafeVisualLayer } from "../../src/components/SafeVisualLayer";
|
|
import type { SafeVisualPrimitive } from "../../src/formats";
|
|
|
|
const visuals: readonly SafeVisualPrimitive[] = [
|
|
{
|
|
type: "line",
|
|
layer: "underlay",
|
|
start: { kind: "coordinate", x: 0.25, y: 0.75 },
|
|
end: { kind: "cell", cell: 5, offsetX: 0.1, offsetY: -0.1 },
|
|
style: { stroke: "#123456", strokeWidth: 0.05, opacity: 0.75 },
|
|
},
|
|
{
|
|
type: "polyline",
|
|
layer: "underlay",
|
|
points: [
|
|
{ kind: "cell", cell: 0 },
|
|
{ kind: "cell", cell: 1 },
|
|
{ kind: "cell", cell: 5 },
|
|
],
|
|
style: { stroke: "#abcdef", fill: "transparent" },
|
|
},
|
|
{
|
|
type: "rectangle",
|
|
layer: "overlay",
|
|
center: { kind: "cell", cell: 6 },
|
|
width: 0.8,
|
|
height: 0.6,
|
|
cornerRadius: 0.1,
|
|
style: { fill: "#ffeecc", stroke: "#112233" },
|
|
},
|
|
{
|
|
type: "ellipse",
|
|
layer: "overlay",
|
|
center: { kind: "coordinate", x: 2.5, y: 2.5 },
|
|
radiusX: 0.4,
|
|
radiusY: 0.2,
|
|
},
|
|
{
|
|
type: "circle",
|
|
layer: "overlay",
|
|
center: { kind: "cell", cell: 10 },
|
|
radius: 0.25,
|
|
},
|
|
{
|
|
type: "text",
|
|
layer: "overlay",
|
|
position: { kind: "cell", cell: 15 },
|
|
text: "source label",
|
|
style: { fill: "#010203", fontSize: 0.4 },
|
|
},
|
|
];
|
|
|
|
describe("SafeVisualLayer", () => {
|
|
it("renders every canonical primitive with stable classes and grid geometry", () => {
|
|
const { container } = render(
|
|
<svg viewBox="0 0 4 4">
|
|
<SafeVisualLayer size={4} visuals={visuals} layer="underlay" />
|
|
<SafeVisualLayer size={4} visuals={visuals} layer="overlay" />
|
|
</svg>,
|
|
);
|
|
|
|
const line = container.querySelector(".source-visual--line");
|
|
expect(line).toHaveAttribute("x1", "0.25");
|
|
expect(line).toHaveAttribute("y1", "0.75");
|
|
expect(line).toHaveAttribute("x2", "1.6");
|
|
expect(line).toHaveAttribute("y2", "1.4");
|
|
expect(line).toHaveAttribute("stroke", "#123456");
|
|
expect(line).toHaveAttribute("stroke-width", "0.05");
|
|
expect(container.querySelector(".source-visual--polyline")).toHaveAttribute(
|
|
"points",
|
|
"0.5,0.5 1.5,0.5 1.5,1.5",
|
|
);
|
|
expect(
|
|
container.querySelector(".source-visual--rectangle"),
|
|
).toHaveAttribute("rx", "0.1");
|
|
expect(container.querySelector(".source-visual--ellipse")).toHaveAttribute(
|
|
"rx",
|
|
"0.4",
|
|
);
|
|
expect(container.querySelector(".source-visual--circle")).toHaveAttribute(
|
|
"r",
|
|
"0.25",
|
|
);
|
|
expect(container.querySelector(".source-visual--text")).toHaveTextContent(
|
|
"source label",
|
|
);
|
|
expect(
|
|
container.querySelectorAll(".safe-visual-layer--underlay > *"),
|
|
).toHaveLength(2);
|
|
expect(
|
|
container.querySelectorAll(".safe-visual-layer--overlay > *"),
|
|
).toHaveLength(4);
|
|
});
|
|
|
|
it("uses a React text node and never creates executable descendants", () => {
|
|
const malicious =
|
|
'</text><script onload="alert(1)">x</script><image href="x">';
|
|
const { container } = render(
|
|
<svg viewBox="0 0 4 4">
|
|
<SafeVisualLayer
|
|
size={4}
|
|
layer="overlay"
|
|
visuals={[
|
|
{
|
|
type: "text",
|
|
layer: "overlay",
|
|
position: { kind: "cell", cell: 0 },
|
|
text: malicious,
|
|
style: { fill: "#000000" },
|
|
},
|
|
]}
|
|
/>
|
|
</svg>,
|
|
);
|
|
|
|
expect(container.querySelector(".source-visual--text")?.textContent).toBe(
|
|
malicious,
|
|
);
|
|
expect(container.querySelector("script, image")).toBeNull();
|
|
expect(
|
|
container.querySelector("[href], [src], [onclick], [onload]"),
|
|
).toBeNull();
|
|
});
|
|
});
|