@@ -239,6 +239,86 @@ tl.fromTo("#box", { opacity: 0, x: -50 }, { opacity: 1, x: 0, duration: 1.5, eas
239239 expect ( result . parsed . animations [ 0 ] . fromProperties ?. x ) . toBe ( - 50 ) ;
240240 } ) ;
241241
242+ it ( "rejects serialized non-finite mutation values before writing source" , async ( ) => {
243+ const projectDir = createProjectDir ( ) ;
244+ writeHtml ( projectDir , "comp.html" , FROMTO_COMP ) ;
245+ const app = new Hono ( ) ;
246+ registerFileRoutes ( app , createAdapter ( projectDir ) ) ;
247+
248+ const anim = await getFirstAnimation ( app , "comp.html" ) ;
249+ const before = readFileSync ( join ( projectDir , "comp.html" ) , "utf-8" ) ;
250+ const res = await app . request ( "http://localhost/projects/demo/gsap-mutations/comp.html" , {
251+ method : "POST" ,
252+ headers : { "Content-Type" : "application/json" } ,
253+ body : JSON . stringify ( {
254+ type : "update-property" ,
255+ animationId : anim . id ,
256+ property : "x" ,
257+ value : Number . NaN ,
258+ } ) ,
259+ } ) ;
260+ const payload = ( await res . json ( ) ) as { error ?: string ; fields ?: string [ ] } ;
261+
262+ expect ( res . status ) . toBe ( 400 ) ;
263+ expect ( payload . error ) . toContain ( "unsafe values" ) ;
264+ expect ( payload . fields ) . toContain ( "body.value" ) ;
265+ expect ( readFileSync ( join ( projectDir , "comp.html" ) , "utf-8" ) ) . toBe ( before ) ;
266+ } ) ;
267+
268+ it ( "rejects unsafe DOM patch metadata before writing source" , async ( ) => {
269+ const projectDir = createProjectDir ( ) ;
270+ writeFileSync ( join ( projectDir , "index.html" ) , '<div id="title">Before</div>' ) ;
271+ const app = new Hono ( ) ;
272+ registerFileRoutes ( app , createAdapter ( projectDir ) ) ;
273+
274+ const response = await app . request (
275+ "http://localhost/projects/demo/file-mutations/patch-element/index.html" ,
276+ {
277+ method : "POST" ,
278+ headers : { "Content-Type" : "application/json" } ,
279+ body : JSON . stringify ( {
280+ target : { id : "title" , selectorIndex : Number . NaN } ,
281+ operations : [ { type : "text-content" , property : "textContent" , value : "After" } ] ,
282+ } ) ,
283+ } ,
284+ ) ;
285+ const payload = ( await response . json ( ) ) as { error ?: string ; fields ?: string [ ] } ;
286+
287+ expect ( response . status ) . toBe ( 400 ) ;
288+ expect ( payload . error ) . toContain ( "unsafe values" ) ;
289+ expect ( payload . fields ) . toContain ( "body.target.selectorIndex" ) ;
290+ expect ( readFileSync ( join ( projectDir , "index.html" ) , "utf-8" ) ) . toBe (
291+ '<div id="title">Before</div>' ,
292+ ) ;
293+ } ) ;
294+
295+ it ( "allows DOM patch null values used for explicit style removals" , async ( ) => {
296+ const projectDir = createProjectDir ( ) ;
297+ writeFileSync (
298+ join ( projectDir , "index.html" ) ,
299+ '<div id="title" style="opacity: 1">Before</div>' ,
300+ ) ;
301+ const app = new Hono ( ) ;
302+ registerFileRoutes ( app , createAdapter ( projectDir ) ) ;
303+
304+ const response = await app . request (
305+ "http://localhost/projects/demo/file-mutations/patch-element/index.html" ,
306+ {
307+ method : "POST" ,
308+ headers : { "Content-Type" : "application/json" } ,
309+ body : JSON . stringify ( {
310+ target : { id : "title" } ,
311+ operations : [ { type : "inline-style" , property : "opacity" , value : null } ] ,
312+ } ) ,
313+ } ,
314+ ) ;
315+ const payload = ( await response . json ( ) ) as { changed ?: boolean ; content ?: string } ;
316+
317+ expect ( response . status ) . toBe ( 200 ) ;
318+ expect ( payload . changed ) . toBe ( true ) ;
319+ expect ( payload . content ) . not . toContain ( "opacity" ) ;
320+ } ) ;
321+
242322 it ( "update-from-property returns 400 for a non-fromTo animation" , async ( ) => {
243323 const projectDir = createProjectDir ( ) ;
244324 const TO_COMP = `<!DOCTYPE html><html><body><script data-hyperframes-gsap>
0 commit comments