@@ -87,6 +87,47 @@ function mockSvgCoordinateSpace(
8787 } ) ;
8888}
8989
90+ function mockElementCoordinateSpace (
91+ svg : SVGSVGElement ,
92+ element : SVGGraphicsElement ,
93+ {
94+ scaleX = 1 ,
95+ scaleY = 1 ,
96+ translateX = 0 ,
97+ translateY = 0 ,
98+ } : {
99+ scaleX ?: number ;
100+ scaleY ?: number ;
101+ translateX ?: number ;
102+ translateY ?: number ;
103+ } = { } ,
104+ ) {
105+ const screenCTM = {
106+ a : scaleX ,
107+ d : scaleY ,
108+ e : translateX ,
109+ f : translateY ,
110+ inverse ( ) {
111+ return {
112+ a : 1 / scaleX ,
113+ d : 1 / scaleY ,
114+ e : - translateX / scaleX ,
115+ f : - translateY / scaleY ,
116+ } ;
117+ } ,
118+ } ;
119+
120+ Object . defineProperty ( element , 'getScreenCTM' , {
121+ configurable : true ,
122+ value : ( ) => screenCTM ,
123+ } ) ;
124+
125+ Object . defineProperty ( element , 'createSVGPoint' , {
126+ configurable : true ,
127+ value : ( ) => svg . createSVGPoint ( ) ,
128+ } ) ;
129+ }
130+
90131describe ( 'exporter/svg' , ( ) => {
91132 beforeEach ( ( ) => {
92133 document . body . innerHTML = '' ;
@@ -371,4 +412,230 @@ describe('exporter/svg', () => {
371412 expect ( exported . getAttribute ( 'width' ) ) . toBe ( '320' ) ;
372413 expect ( exported . getAttribute ( 'height' ) ) . toBe ( '270' ) ;
373414 } ) ;
415+
416+ it ( 'keeps wrapped foreignObject text within the original export width' , async ( ) => {
417+ const svg = document . createElementNS ( svgNS , 'svg' ) ;
418+ svg . setAttribute ( 'viewBox' , '0 0 200 100' ) ;
419+ mockSvgCoordinateSpace ( svg ) ;
420+
421+ const foreignObject = document . createElementNS ( svgNS , 'foreignObject' ) ;
422+ const span = document . createElement ( 'span' ) ;
423+ span . style . width = '100%' ;
424+ span . style . height = '100%' ;
425+ span . style . display = 'flex' ;
426+ span . style . flexWrap = 'wrap' ;
427+ span . style . wordBreak = 'break-word' ;
428+ span . style . whiteSpace = 'pre-wrap' ;
429+
430+ Object . defineProperty ( span , 'scrollWidth' , {
431+ configurable : true ,
432+ get : ( ) => 320 ,
433+ } ) ;
434+ Object . defineProperty ( span , 'scrollHeight' , {
435+ configurable : true ,
436+ get : ( ) => 160 ,
437+ } ) ;
438+
439+ foreignObject . appendChild ( span ) ;
440+ svg . appendChild ( foreignObject ) ;
441+
442+ mockRect ( foreignObject , { left : 0 , top : 0 , width : 200 , height : 100 } ) ;
443+
444+ const exported = await exportToSVG ( svg ) ;
445+
446+ expect ( exported . getAttribute ( 'viewBox' ) ) . toBe ( '0 0 200 160' ) ;
447+ expect ( exported . getAttribute ( 'width' ) ) . toBe ( '200' ) ;
448+ expect ( exported . getAttribute ( 'height' ) ) . toBe ( '160' ) ;
449+ } ) ;
450+
451+ it ( 'resizes exported foreignObject height to the measured wrapped content height' , async ( ) => {
452+ const svg = document . createElementNS ( svgNS , 'svg' ) ;
453+ svg . setAttribute ( 'viewBox' , '0 0 200 100' ) ;
454+ mockSvgCoordinateSpace ( svg ) ;
455+
456+ const foreignObject = document . createElementNS ( svgNS , 'foreignObject' ) ;
457+ foreignObject . setAttribute ( 'x' , '20' ) ;
458+ foreignObject . setAttribute ( 'y' , '30' ) ;
459+ foreignObject . setAttribute ( 'width' , '140' ) ;
460+ foreignObject . setAttribute ( 'height' , '40' ) ;
461+
462+ const span = document . createElement ( 'span' ) ;
463+ span . style . width = '100%' ;
464+ span . style . height = '100%' ;
465+ span . style . display = 'flex' ;
466+ span . style . flexWrap = 'wrap' ;
467+ span . style . wordBreak = 'break-word' ;
468+ span . style . whiteSpace = 'pre-wrap' ;
469+
470+ Object . defineProperty ( span , 'scrollHeight' , {
471+ configurable : true ,
472+ get : ( ) => 80 ,
473+ } ) ;
474+
475+ foreignObject . appendChild ( span ) ;
476+ svg . appendChild ( foreignObject ) ;
477+
478+ mockRect ( foreignObject , { left : 20 , top : 30 , width : 140 , height : 40 } ) ;
479+
480+ const exported = await exportToSVG ( svg ) ;
481+ const exportedForeignObject = exported . querySelector ( 'foreignObject' ) ;
482+
483+ expect ( exportedForeignObject ?. getAttribute ( 'x' ) ) . toBe ( '20' ) ;
484+ expect ( exportedForeignObject ?. getAttribute ( 'y' ) ) . toBe ( '30' ) ;
485+ expect ( exportedForeignObject ?. getAttribute ( 'width' ) ) . toBe ( '140' ) ;
486+ expect ( exportedForeignObject ?. getAttribute ( 'height' ) ) . toBe ( '80' ) ;
487+ } ) ;
488+
489+ it ( 'uses rendered content height when it is larger than scrollHeight' , async ( ) => {
490+ const svg = document . createElementNS ( svgNS , 'svg' ) ;
491+ svg . setAttribute ( 'viewBox' , '0 0 200 100' ) ;
492+ mockSvgCoordinateSpace ( svg ) ;
493+
494+ const foreignObject = document . createElementNS ( svgNS , 'foreignObject' ) ;
495+ foreignObject . setAttribute ( 'x' , '20' ) ;
496+ foreignObject . setAttribute ( 'y' , '30' ) ;
497+ foreignObject . setAttribute ( 'width' , '140' ) ;
498+ foreignObject . setAttribute ( 'height' , '40' ) ;
499+
500+ const span = document . createElement ( 'span' ) ;
501+ span . style . width = '100%' ;
502+ span . style . height = '100%' ;
503+ span . style . display = 'flex' ;
504+ span . style . flexWrap = 'wrap' ;
505+ span . style . wordBreak = 'break-word' ;
506+ span . style . whiteSpace = 'pre-wrap' ;
507+
508+ Object . defineProperty ( span , 'scrollHeight' , {
509+ configurable : true ,
510+ get : ( ) => 80 ,
511+ } ) ;
512+ Object . defineProperty ( span , 'getBoundingClientRect' , {
513+ configurable : true ,
514+ value : ( ) =>
515+ ( {
516+ x : 0 ,
517+ y : 0 ,
518+ left : 0 ,
519+ top : 0 ,
520+ width : 140 ,
521+ height : 84.4 ,
522+ right : 140 ,
523+ bottom : 84.4 ,
524+ toJSON : ( ) => ( { } ) ,
525+ } ) as DOMRect ,
526+ } ) ;
527+
528+ foreignObject . appendChild ( span ) ;
529+ svg . appendChild ( foreignObject ) ;
530+
531+ mockRect ( foreignObject , { left : 20 , top : 30 , width : 140 , height : 40 } ) ;
532+
533+ const exported = await exportToSVG ( svg ) ;
534+ const exportedForeignObject = exported . querySelector ( 'foreignObject' ) ;
535+
536+ expect ( exportedForeignObject ?. getAttribute ( 'height' ) ) . toBe ( '84.4' ) ;
537+ } ) ;
538+
539+ it ( 'keeps foreignObject adjustments aligned when earlier foreignObjects are skipped' , async ( ) => {
540+ const svg = document . createElementNS ( svgNS , 'svg' ) ;
541+ svg . setAttribute ( 'viewBox' , '0 0 200 100' ) ;
542+ mockSvgCoordinateSpace ( svg ) ;
543+
544+ const skippedForeignObject = document . createElementNS ( svgNS , 'foreignObject' ) ;
545+ skippedForeignObject . setAttribute ( 'x' , '0' ) ;
546+ skippedForeignObject . setAttribute ( 'y' , '0' ) ;
547+ skippedForeignObject . setAttribute ( 'width' , '10' ) ;
548+ skippedForeignObject . setAttribute ( 'height' , '10' ) ;
549+ svg . appendChild ( skippedForeignObject ) ;
550+
551+ const adjustedForeignObject = document . createElementNS (
552+ svgNS ,
553+ 'foreignObject' ,
554+ ) ;
555+ adjustedForeignObject . setAttribute ( 'x' , '20' ) ;
556+ adjustedForeignObject . setAttribute ( 'y' , '30' ) ;
557+ adjustedForeignObject . setAttribute ( 'width' , '140' ) ;
558+ adjustedForeignObject . setAttribute ( 'height' , '40' ) ;
559+
560+ const span = document . createElement ( 'span' ) ;
561+ span . style . width = '100%' ;
562+ span . style . height = '100%' ;
563+ span . style . display = 'flex' ;
564+ span . style . flexWrap = 'wrap' ;
565+ span . style . wordBreak = 'break-word' ;
566+ span . style . whiteSpace = 'pre-wrap' ;
567+
568+ Object . defineProperty ( span , 'scrollHeight' , {
569+ configurable : true ,
570+ get : ( ) => 80 ,
571+ } ) ;
572+
573+ adjustedForeignObject . appendChild ( span ) ;
574+ svg . appendChild ( adjustedForeignObject ) ;
575+
576+ mockRect ( skippedForeignObject , { left : 0 , top : 0 , width : 10 , height : 10 } ) ;
577+ mockRect ( adjustedForeignObject , {
578+ left : 20 ,
579+ top : 30 ,
580+ width : 140 ,
581+ height : 40 ,
582+ } ) ;
583+
584+ const exported = await exportToSVG ( svg ) ;
585+ const [ firstForeignObject , secondForeignObject ] =
586+ exported . querySelectorAll ( 'foreignObject' ) ;
587+
588+ expect ( firstForeignObject ?. getAttribute ( 'width' ) ) . toBe ( '10' ) ;
589+ expect ( firstForeignObject ?. getAttribute ( 'height' ) ) . toBe ( '10' ) ;
590+ expect ( secondForeignObject ?. getAttribute ( 'x' ) ) . toBe ( '20' ) ;
591+ expect ( secondForeignObject ?. getAttribute ( 'y' ) ) . toBe ( '30' ) ;
592+ expect ( secondForeignObject ?. getAttribute ( 'width' ) ) . toBe ( '140' ) ;
593+ expect ( secondForeignObject ?. getAttribute ( 'height' ) ) . toBe ( '80' ) ;
594+ } ) ;
595+
596+ it ( 'does not double-apply foreignObject transforms when resizing exported bounds' , async ( ) => {
597+ const svg = document . createElementNS ( svgNS , 'svg' ) ;
598+ svg . setAttribute ( 'viewBox' , '0 0 200 100' ) ;
599+ mockSvgCoordinateSpace ( svg ) ;
600+
601+ const foreignObject = document . createElementNS ( svgNS , 'foreignObject' ) ;
602+ foreignObject . setAttribute ( 'x' , '20' ) ;
603+ foreignObject . setAttribute ( 'y' , '30' ) ;
604+ foreignObject . setAttribute ( 'width' , '140' ) ;
605+ foreignObject . setAttribute ( 'height' , '40' ) ;
606+ foreignObject . setAttribute ( 'transform' , 'translate(5 7)' ) ;
607+
608+ const span = document . createElement ( 'span' ) ;
609+ span . style . width = '100%' ;
610+ span . style . height = '100%' ;
611+ span . style . display = 'flex' ;
612+ span . style . flexWrap = 'wrap' ;
613+ span . style . wordBreak = 'break-word' ;
614+ span . style . whiteSpace = 'pre-wrap' ;
615+
616+ Object . defineProperty ( span , 'scrollHeight' , {
617+ configurable : true ,
618+ get : ( ) => 80 ,
619+ } ) ;
620+
621+ foreignObject . appendChild ( span ) ;
622+ svg . appendChild ( foreignObject ) ;
623+
624+ mockRect ( foreignObject , { left : 25 , top : 37 , width : 140 , height : 40 } ) ;
625+ mockElementCoordinateSpace ( svg , foreignObject as SVGGraphicsElement , {
626+ translateX : 25 ,
627+ translateY : 37 ,
628+ } ) ;
629+
630+ const exported = await exportToSVG ( svg ) ;
631+ const exportedForeignObject = exported . querySelector ( 'foreignObject' ) ;
632+
633+ expect ( exportedForeignObject ?. getAttribute ( 'x' ) ) . toBe ( '20' ) ;
634+ expect ( exportedForeignObject ?. getAttribute ( 'y' ) ) . toBe ( '30' ) ;
635+ expect ( exportedForeignObject ?. getAttribute ( 'width' ) ) . toBe ( '140' ) ;
636+ expect ( exportedForeignObject ?. getAttribute ( 'height' ) ) . toBe ( '80' ) ;
637+ expect ( exportedForeignObject ?. getAttribute ( 'transform' ) ) . toBe (
638+ 'translate(5 7)' ,
639+ ) ;
640+ } ) ;
374641} ) ;
0 commit comments