@@ -785,4 +785,227 @@ describe('beasties', () => {
785785 // Clean up temporary directory
786786 fs . rmSync ( tmpDir , { recursive : true } )
787787 } )
788+
789+ it ( 'removes empty @media blocks when pruneSource is enabled' , async ( ) => {
790+ // Regression test for https://github.com/danielroe/beasties/issues/172
791+ // Empty @media blocks should be removed after pruning, not left behind
792+ const tmpDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'beasties-test-' ) )
793+ fs . writeFileSync ( path . join ( tmpDir , 'style.css' ) , trim `
794+ h1 { color: blue; }
795+ @media (min-width: 768px) {
796+ h1 { padding: 48px; }
797+ }
798+ h2.unused { color: red; }
799+ ` )
800+
801+ const beasties = new Beasties ( {
802+ reduceInlineStyles : false ,
803+ path : tmpDir ,
804+ pruneSource : true ,
805+ } )
806+
807+ let writtenCss = ''
808+ beasties . writeFile = ( filename , data ) => new Promise ( ( resolve , reject ) => {
809+ try {
810+ writtenCss = data
811+ fs . writeFileSync ( filename , data )
812+ resolve ( )
813+ }
814+ catch ( err ) {
815+ reject ( err )
816+ }
817+ } )
818+
819+ const result = await beasties . process ( trim `
820+ <html>
821+ <head>
822+ <link rel="stylesheet" href="/style.css">
823+ </head>
824+ <body>
825+ <h1>Hello World!</h1>
826+ </body>
827+ </html>
828+ ` )
829+
830+ // Critical CSS should include both the base h1 rule and the @media rule
831+ expect ( result ) . toContain ( 'h1{color:blue}' )
832+ expect ( result ) . toContain ( '@media (min-width: 768px)' )
833+ expect ( result ) . toContain ( 'h1{padding:48px}' )
834+
835+ // The pruned CSS file should NOT contain empty @media blocks
836+ expect ( writtenCss ) . not . toContain ( '@media (min-width: 768px){}' )
837+ expect ( writtenCss ) . not . toContain ( '@media (min-width: 768px) {}' )
838+ // It should only contain the unused h2 rule
839+ expect ( writtenCss ) . toEqual ( 'h2.unused{color:red}' )
840+
841+ // Clean up temporary directory
842+ fs . rmSync ( tmpDir , { recursive : true } )
843+ } )
844+
845+ it ( 'removes empty @media blocks from critical CSS when rules go to pruned source' , async ( ) => {
846+ // Regression test for https://github.com/danielroe/beasties/issues/172
847+ // Test the inverse case: empty @media in critical CSS
848+ const tmpDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'beasties-test-' ) )
849+ fs . writeFileSync ( path . join ( tmpDir , 'style.css' ) , trim `
850+ h1 { color: blue; }
851+ @media (min-width: 768px) {
852+ h2.unused { padding: 48px; }
853+ }
854+ ` )
855+
856+ const beasties = new Beasties ( {
857+ reduceInlineStyles : false ,
858+ path : tmpDir ,
859+ pruneSource : true ,
860+ } )
861+
862+ let writtenCss = ''
863+ beasties . writeFile = ( filename , data ) => new Promise ( ( resolve , reject ) => {
864+ try {
865+ writtenCss = data
866+ fs . writeFileSync ( filename , data )
867+ resolve ( )
868+ }
869+ catch ( err ) {
870+ reject ( err )
871+ }
872+ } )
873+
874+ const result = await beasties . process ( trim `
875+ <html>
876+ <head>
877+ <link rel="stylesheet" href="/style.css">
878+ </head>
879+ <body>
880+ <h1>Hello World!</h1>
881+ </body>
882+ </html>
883+ ` )
884+
885+ // Critical CSS should NOT contain empty @media blocks
886+ expect ( result ) . not . toContain ( '@media (min-width: 768px){}' )
887+ expect ( result ) . not . toContain ( '@media (min-width: 768px) {}' )
888+ expect ( result ) . toContain ( 'h1{color:blue}' )
889+
890+ // The pruned CSS should contain the @media block with the unused rule
891+ expect ( writtenCss ) . toContain ( '@media (min-width: 768px)' )
892+ expect ( writtenCss ) . toContain ( 'h2.unused{padding:48px}' )
893+
894+ // Clean up temporary directory
895+ fs . rmSync ( tmpDir , { recursive : true } )
896+ } )
897+
898+ it ( 'removes empty @supports blocks when pruneSource is enabled' , async ( ) => {
899+ // Similar to @media , @supports blocks should be removed when empty
900+ const tmpDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'beasties-test-' ) )
901+ fs . writeFileSync ( path . join ( tmpDir , 'style.css' ) , trim `
902+ h1 { color: blue; }
903+ @supports (display: grid) {
904+ h1 { display: grid; }
905+ }
906+ h2.unused { color: red; }
907+ ` )
908+
909+ const beasties = new Beasties ( {
910+ reduceInlineStyles : false ,
911+ path : tmpDir ,
912+ pruneSource : true ,
913+ } )
914+
915+ let writtenCss = ''
916+ beasties . writeFile = ( filename , data ) => new Promise ( ( resolve , reject ) => {
917+ try {
918+ writtenCss = data
919+ fs . writeFileSync ( filename , data )
920+ resolve ( )
921+ }
922+ catch ( err ) {
923+ reject ( err )
924+ }
925+ } )
926+
927+ const result = await beasties . process ( trim `
928+ <html>
929+ <head>
930+ <link rel="stylesheet" href="/style.css">
931+ </head>
932+ <body>
933+ <h1>Hello World!</h1>
934+ </body>
935+ </html>
936+ ` )
937+
938+ // Critical CSS should include the @supports block with h1 rule
939+ expect ( result ) . toContain ( 'h1{color:blue}' )
940+ expect ( result ) . toContain ( '@supports (display: grid)' )
941+ expect ( result ) . toContain ( 'h1{display:grid}' )
942+
943+ // The pruned CSS file should NOT contain empty @supports blocks
944+ expect ( writtenCss ) . not . toContain ( '@supports' )
945+ // It should only contain the unused h2 rule
946+ expect ( writtenCss ) . toEqual ( 'h2.unused{color:red}' )
947+
948+ // Clean up temporary directory
949+ fs . rmSync ( tmpDir , { recursive : true } )
950+ } )
951+
952+ it ( 'preserves @keyframes when pruneSource is enabled' , async ( ) => {
953+ // @keyframes should be handled as a whole, not recursively walked
954+ const tmpDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'beasties-test-' ) )
955+ fs . writeFileSync ( path . join ( tmpDir , 'style.css' ) , trim `
956+ h1 {
957+ color: blue;
958+ animation: fadeIn 1s;
959+ }
960+ @keyframes fadeIn {
961+ from { opacity: 0; }
962+ to { opacity: 1; }
963+ }
964+ @keyframes unused {
965+ 0% { transform: scale(0); }
966+ 100% { transform: scale(1); }
967+ }
968+ ` )
969+
970+ const beasties = new Beasties ( {
971+ reduceInlineStyles : false ,
972+ path : tmpDir ,
973+ pruneSource : true ,
974+ keyframes : 'critical' ,
975+ } )
976+
977+ let writtenCss = ''
978+ beasties . writeFile = ( filename , data ) => new Promise ( ( resolve , reject ) => {
979+ try {
980+ writtenCss = data
981+ fs . writeFileSync ( filename , data )
982+ resolve ( )
983+ }
984+ catch ( err ) {
985+ reject ( err )
986+ }
987+ } )
988+
989+ const result = await beasties . process ( trim `
990+ <html>
991+ <head>
992+ <link rel="stylesheet" href="/style.css">
993+ </head>
994+ <body>
995+ <h1>Hello World!</h1>
996+ </body>
997+ </html>
998+ ` )
999+
1000+ // Critical CSS should include the used @keyframes
1001+ expect ( result ) . toContain ( '@keyframes fadeIn' )
1002+ expect ( result ) . toContain ( 'from{opacity:0}' )
1003+ expect ( result ) . toContain ( 'to{opacity:1}' )
1004+
1005+ // Unused @keyframes should be in the pruned source
1006+ expect ( writtenCss ) . toContain ( '@keyframes unused' )
1007+
1008+ // Clean up temporary directory
1009+ fs . rmSync ( tmpDir , { recursive : true } )
1010+ } )
7881011} )
0 commit comments