@@ -81,6 +81,114 @@ describe("media rules", () => {
8181 expect ( finding ) . toBeUndefined ( ) ;
8282 } ) ;
8383
84+ it ( "reports grading controls placed outside their schema sections" , async ( ) => {
85+ const html = `
86+ <html><body>
87+ <div id="root" data-composition-id="c1" data-width="1920" data-height="1080">
88+ <video id="v1" data-start="0" data-duration="5" src="clip.mp4" muted data-color-grading='{"preset":"skin-soft","intensity":0.58,"highlights":-0.06,"temperature":0.02}'></video>
89+ </div>
90+ <script>window.__timelines = {};</script>
91+ </body></html>` ;
92+ const result = await lintHyperframeHtml ( html ) ;
93+ const finding = result . findings . find ( ( f ) => f . code === "color_grading_invalid_structure" ) ;
94+ expect ( finding ?. severity ) . toBe ( "error" ) ;
95+ expect ( finding ?. message ) . toContain ( "highlights" ) ;
96+ expect ( finding ?. fixHint ) . toContain ( '"adjust"' ) ;
97+ } ) ;
98+
99+ it ( "accepts grading controls inside their schema sections" , async ( ) => {
100+ const html = `
101+ <html><body>
102+ <div id="root" data-composition-id="c1" data-width="1920" data-height="1080">
103+ <video id="v1" data-start="0" data-duration="5" src="clip.mp4" muted data-color-grading='{"preset":"skin-soft","intensity":0.58,"adjust":{"highlights":-0.06,"temperature":0.02},"details":{"vignette":0.03},"effects":{"blur":0.1,"chromaBleed":0.2,"tapeDamage":0.3,"tapeTracking":0.4,"tapeNoise":0.5,"tapeSpeed":0.6,"filmArtifacts":0.4,"halftone":0.5,"halftoneSize":0.6,"twoInkPrint":0.7,"twoInkPrintSize":0.8,"ascii":0.9,"asciiSize":0.4,"asciiInvert":1,"dither":0.8,"ditherSize":0.3,"bloom":0.5,"bloomRadius":8,"asciiStyle":4,"asciiColor":1,"asciiRotation":1,"monoScreen":0.5,"monoScreenSize":0.4,"monoScreenAngle":0.3,"monoScreenSpread":0.2,"monoScreenShape":3,"monoScreenInvert":1,"scanlines":0.3,"scanlineCount":0.4,"scanlineSoftness":0.5,"chromaticAberration":0.2,"chromaticAngle":0.6,"crtCurvature":0.25,"digitalGlitch":0.4,"digitalGlitchColorSplit":0.45,"digitalGlitchLineTear":0.5,"digitalGlitchPixelate":0.55,"digitalGlitchBlockAmount":0.6,"digitalGlitchBlockDisplacement":0.7,"digitalGlitchBlockOpacity":0.2,"digitalGlitchSpeed":0.7,"engraving":1,"engravingSpacing":0.4117647,"engravingMinThickness":0.2,"engravingMaxThickness":0.4571429,"engravingAngle":0.25,"engravingContrast":0.4666667,"engravingSharpness":0.59,"engravingWave":0.2,"engravingWaveFrequency":0.2222222},"palette":["#ff6b66","#080717","#d9339f","#3c185f"],"lut":null}'></video>
104+ </div>
105+ <script>window.__timelines = {};</script>
106+ </body></html>` ;
107+ const result = await lintHyperframeHtml ( html ) ;
108+ expect ( result . findings . find ( ( f ) => f . code . startsWith ( "color_grading_" ) ) ) . toBeUndefined ( ) ;
109+ } ) ;
110+
111+ it ( "accepts crosshatch controls in the effects section" , async ( ) => {
112+ const html = `
113+ <html><body>
114+ <div id="root" data-composition-id="c1" data-width="1920" data-height="1080">
115+ <video data-start="0" data-duration="5" src="clip.mp4" muted data-color-grading='{"effects":{"crosshatch":1,"crosshatchSpacing":0.28,"crosshatchThickness":0.25,"crosshatchAngle":0.25,"crosshatchContrast":0.3333333,"crosshatchEdges":0.5,"crosshatchLineWeight":0,"crosshatchWave":0.33,"crosshatchWaveFrequency":0.2222222}}'></video>
116+ </div>
117+ <script>window.__timelines = {};</script>
118+ </body></html>` ;
119+ const result = await lintHyperframeHtml ( html ) ;
120+ expect ( result . findings . find ( ( f ) => f . code . startsWith ( "color_grading_" ) ) ) . toBeUndefined ( ) ;
121+ } ) ;
122+
123+ it ( "accepts Kuwahara controls in the effects section" , async ( ) => {
124+ const html = `
125+ <html><body>
126+ <div id="root" data-composition-id="c1" data-width="1920" data-height="1080">
127+ <video data-start="0" data-duration="5" src="clip.mp4" muted data-color-grading='{"effects":{"kuwahara":1,"kuwaharaRadius":0.142857,"kuwaharaSharpness":0.3125,"kuwaharaSaturation":0.5}}'></video>
128+ </div>
129+ <script>window.__timelines = {};</script>
130+ </body></html>` ;
131+ const result = await lintHyperframeHtml ( html ) ;
132+ expect ( result . findings . find ( ( f ) => f . code . startsWith ( "color_grading_" ) ) ) . toBeUndefined ( ) ;
133+ } ) ;
134+
135+ it ( "reports malformed or out-of-range color grading palettes" , async ( ) => {
136+ const html = `
137+ <html><body>
138+ <div id="root" data-composition-id="c1" data-width="1920" data-height="1080">
139+ <video id="v1" data-start="0" data-duration="5" src="clip.mp4" muted data-color-grading='{"effects":{"dither":1},"palette":["#000000","red"]}'></video>
140+ </div>
141+ <script>window.__timelines = {};</script>
142+ </body></html>` ;
143+ const result = await lintHyperframeHtml ( html ) ;
144+ const finding = result . findings . find ( ( f ) => f . code === "color_grading_invalid_structure" ) ;
145+ expect ( finding ?. severity ) . toBe ( "error" ) ;
146+ expect ( finding ?. fixHint ) . toContain ( "2 to 6" ) ;
147+ expect ( finding ?. fixHint ) . toContain ( "#RRGGBB" ) ;
148+ } ) ;
149+
150+ it ( "reports malformed grading JSON" , async ( ) => {
151+ const html = `
152+ <html><body>
153+ <div id="root" data-composition-id="c1" data-width="1920" data-height="1080">
154+ <video id="v1" data-start="0" data-duration="5" src="clip.mp4" muted data-color-grading='{"preset":"skin-soft"'></video>
155+ </div>
156+ <script>window.__timelines = {};</script>
157+ </body></html>` ;
158+ const result = await lintHyperframeHtml ( html ) ;
159+ expect ( result . findings . find ( ( f ) => f . code === "color_grading_invalid_json" ) ?. severity ) . toBe (
160+ "error" ,
161+ ) ;
162+ } ) ;
163+
164+ it ( "reports invalid string values for structured grading sections" , async ( ) => {
165+ const html = `
166+ <html><body>
167+ <div id="root" data-composition-id="c1" data-width="1920" data-height="1080">
168+ <video id="v1" data-start="0" data-duration="5" src="clip.mp4" muted data-color-grading='{"adjust":"cinematic"}'></video>
169+ </div>
170+ <script>window.__timelines = {};</script>
171+ </body></html>` ;
172+ const result = await lintHyperframeHtml ( html ) ;
173+ expect (
174+ result . findings . find ( ( f ) => f . code === "color_grading_invalid_structure" ) ?. severity ,
175+ ) . toBe ( "error" ) ;
176+ } ) ;
177+
178+ it ( "reports color grading on non-media elements" , async ( ) => {
179+ const html = `
180+ <html><body>
181+ <div id="root" data-composition-id="c1" data-width="1920" data-height="1080">
182+ <div id="background" data-color-grading='{"preset":"skin-soft"}'></div>
183+ </div>
184+ <script>window.__timelines = {};</script>
185+ </body></html>` ;
186+ const result = await lintHyperframeHtml ( html ) ;
187+ expect ( result . findings . find ( ( f ) => f . code === "color_grading_non_media" ) ?. severity ) . toBe (
188+ "error" ,
189+ ) ;
190+ } ) ;
191+
84192 it ( "reports warning for media with preload=none" , async ( ) => {
85193 const html = `
86194<html><body>
0 commit comments