@@ -735,23 +735,71 @@ describe('React Native SVG Processing - RNSvgHandler', () => {
735735 } ) ;
736736
737737 describe ( 'Error Handling' , ( ) => {
738- it ( 'should warn for unsupported element names but still include them in output' , ( ) => {
738+ it ( 'should warn for unsupported element names and remove them from output' , ( ) => {
739739 const warnSpy = jest . spyOn ( console , 'warn' ) . mockImplementation ( ) ;
740740
741741 const input = '<Svg><UnsupportedElement x="10" y="10" /></Svg>' ;
742742 const output = transformSvg ( input ) ;
743743
744- // Unsupported elements are converted to lowercase and included
744+ // Unsupported elements are now removed entirely rather than left in place.
745745 expect ( output ) . toMatchInlineSnapshot (
746- `"<svg xmlns="http://www.w3.org/2000/svg"><unsupportedElement x="10" y="10" />< /svg>"`
746+ `"<svg xmlns="http://www.w3.org/2000/svg"></svg>"`
747747 ) ;
748748 expect ( warnSpy ) . toHaveBeenCalledWith (
749- expect . stringContaining ( 'Skipping unsupported element' )
749+ expect . stringContaining ( 'Removing unsupported element' )
750750 ) ;
751751
752752 warnSpy . mockRestore ( ) ;
753753 } ) ;
754754
755+ it ( 'should remove an unsupported element but keep its supported siblings' , ( ) => {
756+ const input =
757+ '<Svg><Circle cx="50" cy="50" r="40" fill="#27ae60" /><UnsupportedElement x="10" y="10" /></Svg>' ;
758+ const output = transformSvg ( input ) ;
759+
760+ expect ( output ) . toContain (
761+ '<circle cx="50" cy="50" r="40" fill="#27ae60" />'
762+ ) ;
763+ expect ( output ) . not . toContain ( 'unsupportedElement' ) ;
764+ } ) ;
765+
766+ it ( 'should remove an unsupported element with dynamic/unresolvable attributes without throwing' , ( ) => {
767+ // Mirrors the customer repro: a custom component (e.g. AnimatedPath from
768+ // Animated.createAnimatedComponent) with JSX expression container attributes
769+ // that cannot be statically resolved (fill={color}, style={{opacity}}).
770+ const input = `
771+ function Icon({ color, opacity }) {
772+ return (
773+ <Svg width="64" height="64" viewBox="0 0 64 64">
774+ <Circle cx="32" cy="32" r="28" fill="#27ae60" />
775+ <AnimatedPath
776+ d="M18 32 L28 42 L46 20"
777+ fill={color}
778+ style={{ opacity }}
779+ />
780+ </Svg>
781+ );
782+ }
783+ ` ;
784+
785+ expect ( ( ) => transformSvg ( input ) ) . not . toThrow ( ) ;
786+
787+ const output = transformSvg ( input ) ;
788+ expect ( output ) . toContain (
789+ '<circle cx="32" cy="32" r="28" fill="#27ae60" />'
790+ ) ;
791+ expect ( output ) . not . toContain ( 'animatedPath' ) ;
792+ expect ( output ) . not . toContain ( 'fill={color}' ) ;
793+ } ) ;
794+
795+ it ( 'should preserve resolvable falsy values (0) instead of treating them as unresolved' , ( ) => {
796+ const input =
797+ '<Svg><Circle cx="50" cy="50" r="40" opacity={0} /></Svg>' ;
798+ const output = transformSvg ( input ) ;
799+
800+ expect ( output ) . toContain ( 'opacity="0"' ) ;
801+ } ) ;
802+
755803 it ( 'should handle malformed transform array gracefully' , ( ) => {
756804 const input =
757805 '<Svg><Rect x="10" y="10" width="50" height="50" transform={[null, undefined, { invalid: true }]} /></Svg>' ;
@@ -826,4 +874,68 @@ describe('SessionReplayView.Privacy SVG Wrapper', () => {
826874 expect ( output ) . not . toContain ( 'flexShrink' ) ;
827875 expect ( output ) . not . toContain ( 'style' ) ;
828876 } ) ;
877+
878+ it ( 'should capture an SVG whose child element has a non-resolvable prop, dropping only that attr from the asset' , ( ) => {
879+ const assetDir = path . join ( os . tmpdir ( ) , 'dd-svg-test-assets' ) ;
880+ const input = `
881+ function Icon({ color }) {
882+ return (
883+ <Svg width="80" height="80" viewBox="0 0 100 100">
884+ <Circle cx={50} cy={50} r={40} fill={color} />
885+ </Svg>
886+ );
887+ }
888+ ` ;
889+ const output = transformWithSvgTracking ( input ) ;
890+
891+ // The wrapper must be present — the element was NOT silently dropped
892+ expect ( output ) . toContain ( 'SessionReplayView.Privacy' ) ;
893+ // The wrapper carries a hash attribute — the SVG passed through svgo without error
894+ expect ( output ) . toContain ( 'hash:' ) ;
895+
896+ // The written SVG asset must not contain the unresolvable expression.
897+ // (Reading the file proves svgo successfully processed the content.)
898+ const svgFiles = fs
899+ . readdirSync ( assetDir )
900+ . filter ( f => f . endsWith ( '.svg' ) ) ;
901+ expect ( svgFiles . length ) . toBeGreaterThan ( 0 ) ;
902+ const svgContent = fs . readFileSync (
903+ path . join ( assetDir , svgFiles [ svgFiles . length - 1 ] ) ,
904+ 'utf8'
905+ ) ;
906+ expect ( svgContent ) . not . toContain ( 'fill={color}' ) ;
907+ // Numeric literals on child elements are resolved and kept
908+ expect ( svgContent ) . toContain ( 'cx="50"' ) ;
909+ expect ( svgContent ) . toContain ( 'cy="50"' ) ;
910+ expect ( svgContent ) . toContain ( 'r="40"' ) ;
911+ } ) ;
912+
913+ it ( 'should capture an SVG where all child props are non-resolvable, producing a shape-only SVG' , ( ) => {
914+ const assetDir = path . join ( os . tmpdir ( ) , 'dd-svg-test-assets' ) ;
915+ const input = `
916+ function Icon({ d, fill, strokeWidth }) {
917+ return (
918+ <Svg width="24" height="24" viewBox="0 0 24 24">
919+ <Path d={d} fill={fill} strokeWidth={strokeWidth} />
920+ </Svg>
921+ );
922+ }
923+ ` ;
924+ const output = transformWithSvgTracking ( input ) ;
925+
926+ // Still wrapped — the SVG container is captured even if all child attrs are dynamic
927+ expect ( output ) . toContain ( 'SessionReplayView.Privacy' ) ;
928+
929+ const svgFiles = fs
930+ . readdirSync ( assetDir )
931+ . filter ( f => f . endsWith ( '.svg' ) ) ;
932+ expect ( svgFiles . length ) . toBeGreaterThan ( 0 ) ;
933+ const svgContent = fs . readFileSync (
934+ path . join ( assetDir , svgFiles [ svgFiles . length - 1 ] ) ,
935+ 'utf8'
936+ ) ;
937+ expect ( svgContent ) . not . toContain ( 'fill={fill}' ) ;
938+ expect ( svgContent ) . not . toContain ( 'd={d}' ) ;
939+ expect ( svgContent ) . not . toContain ( 'strokeWidth={strokeWidth}' ) ;
940+ } ) ;
829941} ) ;
0 commit comments