@@ -119,6 +119,124 @@ describe('font-scaling-lab — reported issues', () => {
119119 expect ( culprit . suggestedFixes [ 0 ] . title ) . toMatch ( / w i d t h / i) ;
120120 } ) ;
121121
122+ it ( 'suggests a width fix when the overflow is horizontal, not height' , async ( ) => {
123+ // Reported case: an input whose text overflows horizontally, inside a container
124+ // whose FIXED axis is height (its width fills the modal). The advice must address
125+ // the axis that actually overflowed (width), not the axis that happens to be fixed.
126+ const text = makeText ( 'A long input value that does not fit on one line' , {
127+ fontSize : 14 , fontName : { family : 'Inter' , style : 'Regular' } ,
128+ width : 300 , height : 20 , textAutoResize : 'HEIGHT' ,
129+ } ) ;
130+ const input = makeNode ( 'FRAME' , {
131+ name : 'Input' , width : 100 , height : 40 ,
132+ layoutSizingHorizontal : 'FILL' , layoutSizingVertical : 'FIXED' ,
133+ } ) ;
134+ input . appendChild ( text ) ;
135+ const frame = makeNode ( 'FRAME' , { name : 'Modal' , width : 320 , height : 200 } ) ;
136+ frame . appendChild ( input ) ;
137+
138+ frame . absoluteBoundingBox = bbox ( 0 , 0 , 320 , 200 ) ;
139+ input . absoluteBoundingBox = bbox ( 0 , 0 , 100 , 40 ) ;
140+ text . absoluteBoundingBox = bbox ( 0 , 0 , 300 , 20 ) ; // spills out horizontally, not vertically
141+
142+ const { lastOf } = await previewWith ( frame ) ;
143+ const issues = lastOf ( 'preview-result' ) . issues ;
144+
145+ expect ( issues . length ) . toBeGreaterThan ( 0 ) ;
146+ const culprit = issues [ 0 ] ;
147+ expect ( culprit . suggestedFixes [ 0 ] . title ) . toMatch ( / w i d t h / i) ;
148+ expect ( culprit . suggestedFixes [ 0 ] . title ) . not . toMatch ( / h e i g h t / i) ;
149+ } ) ;
150+
151+ it ( 'names the tightest cap up the chain as the root cause, not the immediate parent' , async ( ) => {
152+ // The reported case: flexible text overflows horizontally; the immediate parents
153+ // are Hug; the real constraint is a maxWidth on a container several levels up.
154+ const text = makeText ( 'A value long enough to overflow the capped container' , {
155+ fontSize : 14 , fontName : { family : 'Inter' , style : 'Regular' } ,
156+ width : 500 , height : 20 , textAutoResize : 'WIDTH_AND_HEIGHT' ,
157+ } ) ;
158+ const hug1 = makeNode ( 'FRAME' , { name : 'Row' , layoutSizingHorizontal : 'HUG' , width : 500 , height : 20 } ) ;
159+ const hug2 = makeNode ( 'FRAME' , { name : 'Group' , layoutSizingHorizontal : 'HUG' , width : 500 , height : 20 } ) ;
160+ const capped = makeNode ( 'FRAME' , { name : 'Card' , layoutSizingHorizontal : 'HUG' , maxWidth : 300 , width : 300 , height : 40 } ) ;
161+ const modal = makeNode ( 'FRAME' , { name : 'Modal' , layoutSizingHorizontal : 'FIXED' , width : 360 , height : 200 } ) ;
162+
163+ hug1 . appendChild ( text ) ;
164+ hug2 . appendChild ( hug1 ) ;
165+ capped . appendChild ( hug2 ) ;
166+ modal . appendChild ( capped ) ;
167+
168+ modal . absoluteBoundingBox = bbox ( 0 , 0 , 360 , 200 ) ;
169+ capped . absoluteBoundingBox = bbox ( 0 , 0 , 300 , 40 ) ; // capped at its maxWidth
170+ hug2 . absoluteBoundingBox = bbox ( 0 , 0 , 500 , 20 ) ; // grows with content
171+ hug1 . absoluteBoundingBox = bbox ( 0 , 0 , 500 , 20 ) ;
172+ text . absoluteBoundingBox = bbox ( 0 , 0 , 500 , 20 ) ; // overflows the 300-capped Card
173+
174+ const { lastOf } = await previewWith ( modal ) ;
175+ const issues = lastOf ( 'preview-result' ) . issues ;
176+
177+ expect ( issues . length ) . toBeGreaterThan ( 0 ) ;
178+ const culprit = issues [ 0 ] ;
179+ // The Card (maxWidth 300) is tighter than the Modal (fixed 360) — it wins over
180+ // both the Hug parents and the wider fixed ancestor.
181+ expect ( culprit . name ) . toBe ( 'Card' ) ;
182+ expect ( culprit . suggestedFixes [ 0 ] . title ) . toMatch ( / m a x w i d t h / i) ;
183+ expect ( culprit . suggestedFixes [ 0 ] . description ) . toMatch ( / 3 0 0 / ) ; // names the actual cap value
184+ } ) ;
185+
186+ it ( 'flags the selected root frame itself when it is the fixed constraint' , async ( ) => {
187+ // The reported instance case, reduced: the selected frame is the only thing that
188+ // cannot grow; every container in between hugs, so the content overflows only the
189+ // root. The root was previously excluded from the check, so nothing was reported.
190+ const text = makeText ( 'A value wide enough to overflow the fixed root frame' , {
191+ fontSize : 14 , fontName : { family : 'Inter' , style : 'Regular' } ,
192+ width : 500 , height : 20 , textAutoResize : 'WIDTH_AND_HEIGHT' ,
193+ } ) ;
194+ const hug = makeNode ( 'FRAME' , { name : 'Row' , layoutSizingHorizontal : 'HUG' , width : 500 , height : 20 } ) ;
195+ const root = makeNode ( 'FRAME' , { name : 'Card' , layoutSizingHorizontal : 'FIXED' , width : 300 , height : 100 } ) ;
196+ hug . appendChild ( text ) ;
197+ root . appendChild ( hug ) ;
198+
199+ root . absoluteBoundingBox = bbox ( 0 , 0 , 300 , 100 ) ;
200+ hug . absoluteBoundingBox = bbox ( 0 , 0 , 500 , 20 ) ; // hugs the content, overflowing the root
201+ text . absoluteBoundingBox = bbox ( 0 , 0 , 500 , 20 ) ;
202+
203+ const { lastOf } = await previewWith ( root ) ;
204+ const issues = lastOf ( 'preview-result' ) . issues ;
205+
206+ expect ( issues . length ) . toBeGreaterThan ( 0 ) ; // previously zero: the root was skipped
207+ expect ( issues [ 0 ] . name ) . toBe ( 'Card' ) ; // the selected root is the culprit
208+ expect ( issues [ 0 ] . suggestedFixes [ 0 ] . title ) . toMatch ( / w i d t h / i) ;
209+ } ) ;
210+
211+ it ( 'does not flag a FILL root as fixed-width (its size comes from its parent)' , async ( ) => {
212+ // A layer that FILLS its parent renders at a fixed size when previewed off-canvas
213+ // (FILL needs an auto-layout parent). That is a preview artifact, not a real fixed
214+ // width — the plugin must read the ORIGINAL sizing and not report it as clipped.
215+ const text = makeText ( 'A value that would overflow if the root were fixed' , {
216+ fontSize : 14 , fontName : { family : 'Inter' , style : 'Regular' } ,
217+ width : 500 , height : 20 , textAutoResize : 'WIDTH_AND_HEIGHT' ,
218+ } ) ;
219+ const hug = makeNode ( 'FRAME' , { name : 'Row' , layoutSizingHorizontal : 'HUG' , width : 500 , height : 20 } ) ;
220+ // The selected root fills its parent horizontally; only its height is fixed.
221+ const root = makeNode ( 'FRAME' , {
222+ name : 'Card' , layoutSizingHorizontal : 'FILL' , layoutSizingVertical : 'FIXED' ,
223+ width : 392 , height : 32 ,
224+ } ) ;
225+ hug . appendChild ( text ) ;
226+ root . appendChild ( hug ) ;
227+
228+ root . absoluteBoundingBox = bbox ( 0 , 0 , 392 , 32 ) ;
229+ hug . absoluteBoundingBox = bbox ( 0 , 0 , 500 , 20 ) ;
230+ text . absoluteBoundingBox = bbox ( 0 , 0 , 500 , 20 ) ; // "overflows" the 392 root horizontally
231+
232+ const { lastOf } = await previewWith ( root ) ;
233+ const issues = lastOf ( 'preview-result' ) . issues ;
234+
235+ // The only horizontal constraint is the FILL root, which is a preview artifact.
236+ const widthClaims = issues . filter ( ( i ) => / f i x e d w i d t h / i. test ( i . description || '' ) ) ;
237+ expect ( widthClaims ) . toEqual ( [ ] ) ;
238+ } ) ;
239+
122240 it ( 'reports nothing when everything still fits' , async ( ) => {
123241 const text = makeText ( 'Short' , {
124242 fontSize : 14 , fontName : { family : 'Inter' , style : 'Regular' } , width : 60 , height : 20 ,
0 commit comments