|
1 | 1 | import { afterEach, describe, expect, it } from "vitest"; |
2 | 2 | import { Hono } from "hono"; |
3 | | -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; |
| 3 | +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; |
4 | 4 | import { tmpdir } from "node:os"; |
5 | 5 | import { join } from "node:path"; |
6 | 6 | import { registerFileRoutes } from "./files"; |
@@ -171,6 +171,86 @@ tl.fromTo("#box", { opacity: 0, x: -50 }, { opacity: 1, x: 0, duration: 1.5, eas |
171 | 171 | expect(result.parsed.animations[0].fromProperties?.x).toBe(-50); |
172 | 172 | }); |
173 | 173 |
|
| 174 | + it("rejects serialized non-finite mutation values before writing source", async () => { |
| 175 | + const projectDir = createProjectDir(); |
| 176 | + writeHtml(projectDir, "comp.html", FROMTO_COMP); |
| 177 | + const app = new Hono(); |
| 178 | + registerFileRoutes(app, createAdapter(projectDir)); |
| 179 | + |
| 180 | + const anim = await getFirstAnimation(app, "comp.html"); |
| 181 | + const before = readFileSync(join(projectDir, "comp.html"), "utf-8"); |
| 182 | + const res = await app.request("http://localhost/projects/demo/gsap-mutations/comp.html", { |
| 183 | + method: "POST", |
| 184 | + headers: { "Content-Type": "application/json" }, |
| 185 | + body: JSON.stringify({ |
| 186 | + type: "update-property", |
| 187 | + animationId: anim.id, |
| 188 | + property: "x", |
| 189 | + value: Number.NaN, |
| 190 | + }), |
| 191 | + }); |
| 192 | + const payload = (await res.json()) as { error?: string; fields?: string[] }; |
| 193 | + |
| 194 | + expect(res.status).toBe(400); |
| 195 | + expect(payload.error).toContain("unsafe values"); |
| 196 | + expect(payload.fields).toContain("body.value"); |
| 197 | + expect(readFileSync(join(projectDir, "comp.html"), "utf-8")).toBe(before); |
| 198 | + }); |
| 199 | + |
| 200 | + it("rejects unsafe DOM patch metadata before writing source", async () => { |
| 201 | + const projectDir = createProjectDir(); |
| 202 | + writeFileSync(join(projectDir, "index.html"), '<div id="title">Before</div>'); |
| 203 | + const app = new Hono(); |
| 204 | + registerFileRoutes(app, createAdapter(projectDir)); |
| 205 | + |
| 206 | + const response = await app.request( |
| 207 | + "http://localhost/projects/demo/file-mutations/patch-element/index.html", |
| 208 | + { |
| 209 | + method: "POST", |
| 210 | + headers: { "Content-Type": "application/json" }, |
| 211 | + body: JSON.stringify({ |
| 212 | + target: { id: "title", selectorIndex: Number.NaN }, |
| 213 | + operations: [{ type: "text-content", property: "textContent", value: "After" }], |
| 214 | + }), |
| 215 | + }, |
| 216 | + ); |
| 217 | + const payload = (await response.json()) as { error?: string; fields?: string[] }; |
| 218 | + |
| 219 | + expect(response.status).toBe(400); |
| 220 | + expect(payload.error).toContain("unsafe values"); |
| 221 | + expect(payload.fields).toContain("body.target.selectorIndex"); |
| 222 | + expect(readFileSync(join(projectDir, "index.html"), "utf-8")).toBe( |
| 223 | + '<div id="title">Before</div>', |
| 224 | + ); |
| 225 | + }); |
| 226 | + |
| 227 | + it("allows DOM patch null values used for explicit style removals", async () => { |
| 228 | + const projectDir = createProjectDir(); |
| 229 | + writeFileSync( |
| 230 | + join(projectDir, "index.html"), |
| 231 | + '<div id="title" style="opacity: 1">Before</div>', |
| 232 | + ); |
| 233 | + const app = new Hono(); |
| 234 | + registerFileRoutes(app, createAdapter(projectDir)); |
| 235 | + |
| 236 | + const response = await app.request( |
| 237 | + "http://localhost/projects/demo/file-mutations/patch-element/index.html", |
| 238 | + { |
| 239 | + method: "POST", |
| 240 | + headers: { "Content-Type": "application/json" }, |
| 241 | + body: JSON.stringify({ |
| 242 | + target: { id: "title" }, |
| 243 | + operations: [{ type: "inline-style", property: "opacity", value: null }], |
| 244 | + }), |
| 245 | + }, |
| 246 | + ); |
| 247 | + const payload = (await response.json()) as { changed?: boolean; content?: string }; |
| 248 | + |
| 249 | + expect(response.status).toBe(200); |
| 250 | + expect(payload.changed).toBe(true); |
| 251 | + expect(payload.content).not.toContain("opacity"); |
| 252 | + }); |
| 253 | + |
174 | 254 | it("update-from-property returns 400 for a non-fromTo animation", async () => { |
175 | 255 | const projectDir = createProjectDir(); |
176 | 256 | const TO_COMP = `<!DOCTYPE html><html><body><script data-hyperframes-gsap> |
|
0 commit comments