Files
sudoku-tools/tests/components/safeVisualLayer.test.tsx
T

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();
});
});