@@ -770,4 +770,275 @@ describe('CSSNode', () => {
770770 } )
771771 } )
772772 } )
773+
774+ describe ( 'Compound selector helpers' , ( ) => {
775+ describe ( 'compound_parts() iterator' , ( ) => {
776+ test ( 'yields parts before first combinator' , ( ) => {
777+ const result = parse_selector ( 'div.foo#bar > p' )
778+ const selector = result . first_child !
779+
780+ const parts = Array . from ( selector . compound_parts ( ) )
781+ expect ( parts . length ) . toBe ( 3 )
782+ expect ( parts [ 0 ] . text ) . toBe ( 'div' )
783+ expect ( parts [ 1 ] . text ) . toBe ( '.foo' )
784+ expect ( parts [ 2 ] . text ) . toBe ( '#bar' )
785+ } )
786+
787+ test ( 'zero allocations for iteration' , ( ) => {
788+ const result = parse_selector ( 'div.foo > p' )
789+ const selector = result . first_child !
790+
791+ let count = 0
792+ for ( const _part of selector . compound_parts ( ) ) {
793+ count ++
794+ }
795+ expect ( count ) . toBe ( 2 )
796+ } )
797+
798+ test ( 'returns empty for wrong type' , ( ) => {
799+ const result = parse_selector ( 'div' )
800+ const list = result // NODE_SELECTOR_LIST
801+
802+ const parts = Array . from ( list . compound_parts ( ) )
803+ expect ( parts . length ) . toBe ( 0 )
804+ } )
805+
806+ test ( 'works with all parts when no combinator' , ( ) => {
807+ const result = parse_selector ( 'div.foo#bar' )
808+ const selector = result . first_child !
809+
810+ const parts = Array . from ( selector . compound_parts ( ) )
811+ expect ( parts . length ) . toBe ( 3 )
812+ } )
813+
814+ test ( 'handles leading combinator (CSS Nesting)' , ( ) => {
815+ const result = parse_selector ( '> p' )
816+ const selector = result . first_child !
817+
818+ const parts = Array . from ( selector . compound_parts ( ) )
819+ expect ( parts . length ) . toBe ( 0 ) // No parts before combinator
820+ } )
821+
822+ test ( 'works with pseudo-classes' , ( ) => {
823+ const result = parse_selector ( 'a.link:hover > p' )
824+ const selector = result . first_child !
825+
826+ const parts = Array . from ( selector . compound_parts ( ) )
827+ expect ( parts . length ) . toBe ( 3 )
828+ expect ( parts [ 0 ] . text ) . toBe ( 'a' )
829+ expect ( parts [ 1 ] . text ) . toBe ( '.link' )
830+ expect ( parts [ 2 ] . text ) . toBe ( ':hover' )
831+ } )
832+ } )
833+
834+ describe ( 'first_compound property' , ( ) => {
835+ test ( 'returns array of parts before combinator' , ( ) => {
836+ const result = parse_selector ( 'div.foo#bar > p' )
837+ const selector = result . first_child !
838+
839+ const compound = selector . first_compound
840+ expect ( compound . length ) . toBe ( 3 )
841+ expect ( compound [ 0 ] . text ) . toBe ( 'div' )
842+ expect ( compound [ 1 ] . text ) . toBe ( '.foo' )
843+ expect ( compound [ 2 ] . text ) . toBe ( '#bar' )
844+ } )
845+
846+ test ( 'returns all parts when no combinators' , ( ) => {
847+ const result = parse_selector ( 'div.foo#bar' )
848+ const selector = result . first_child !
849+
850+ const compound = selector . first_compound
851+ expect ( compound . length ) . toBe ( 3 )
852+ } )
853+
854+ test ( 'returns empty array for wrong type' , ( ) => {
855+ const result = parse_selector ( 'div' )
856+ expect ( result . first_compound ) . toEqual ( [ ] )
857+ } )
858+
859+ test ( 'handles attribute selectors' , ( ) => {
860+ const result = parse_selector ( 'input[type="text"]:focus + label' )
861+ const selector = result . first_child !
862+
863+ const compound = selector . first_compound
864+ expect ( compound . length ) . toBe ( 3 )
865+ expect ( compound [ 0 ] . text ) . toBe ( 'input' )
866+ expect ( compound [ 1 ] . text ) . toBe ( '[type="text"]' )
867+ expect ( compound [ 2 ] . text ) . toBe ( ':focus' )
868+ } )
869+
870+ test ( 'handles leading combinator' , ( ) => {
871+ const result = parse_selector ( '> div' )
872+ const selector = result . first_child !
873+
874+ const compound = selector . first_compound
875+ expect ( compound . length ) . toBe ( 0 )
876+ } )
877+ } )
878+
879+ describe ( 'all_compounds property' , ( ) => {
880+ test ( 'splits by combinators' , ( ) => {
881+ const result = parse_selector ( 'div.foo > p.bar + span' )
882+ const selector = result . first_child !
883+
884+ const compounds = selector . all_compounds
885+ expect ( compounds . length ) . toBe ( 3 )
886+ expect ( compounds [ 0 ] . length ) . toBe ( 2 ) // div, .foo
887+ expect ( compounds [ 1 ] . length ) . toBe ( 2 ) // p, .bar
888+ expect ( compounds [ 2 ] . length ) . toBe ( 1 ) // span
889+ } )
890+
891+ test ( 'handles single compound (no combinators)' , ( ) => {
892+ const result = parse_selector ( 'div.foo#bar' )
893+ const selector = result . first_child !
894+
895+ const compounds = selector . all_compounds
896+ expect ( compounds . length ) . toBe ( 1 )
897+ expect ( compounds [ 0 ] . length ) . toBe ( 3 )
898+ } )
899+
900+ test ( 'handles leading combinator' , ( ) => {
901+ const result = parse_selector ( '> p' )
902+ const selector = result . first_child !
903+
904+ const compounds = selector . all_compounds
905+ expect ( compounds . length ) . toBe ( 1 )
906+ expect ( compounds [ 0 ] . length ) . toBe ( 1 )
907+ expect ( compounds [ 0 ] [ 0 ] . text ) . toBe ( 'p' )
908+ } )
909+
910+ test ( 'handles multiple combinators' , ( ) => {
911+ const result = parse_selector ( 'a > b + c ~ d' )
912+ const selector = result . first_child !
913+
914+ const compounds = selector . all_compounds
915+ expect ( compounds . length ) . toBe ( 4 )
916+ expect ( compounds [ 0 ] [ 0 ] . text ) . toBe ( 'a' )
917+ expect ( compounds [ 1 ] [ 0 ] . text ) . toBe ( 'b' )
918+ expect ( compounds [ 2 ] [ 0 ] . text ) . toBe ( 'c' )
919+ expect ( compounds [ 3 ] [ 0 ] . text ) . toBe ( 'd' )
920+ } )
921+
922+ test ( 'handles descendant combinator (space)' , ( ) => {
923+ const result = parse_selector ( 'div p span' )
924+ const selector = result . first_child !
925+
926+ const compounds = selector . all_compounds
927+ expect ( compounds . length ) . toBe ( 3 )
928+ } )
929+
930+ test ( 'returns empty array for wrong type' , ( ) => {
931+ const result = parse_selector ( 'div' )
932+ expect ( result . all_compounds ) . toEqual ( [ ] )
933+ } )
934+ } )
935+
936+ describe ( 'is_compound property' , ( ) => {
937+ test ( 'true when no combinators' , ( ) => {
938+ const result = parse_selector ( 'div.foo#bar' )
939+ const selector = result . first_child !
940+ expect ( selector . is_compound ) . toBe ( true )
941+ } )
942+
943+ test ( 'false when has combinators' , ( ) => {
944+ const result = parse_selector ( 'div > p' )
945+ const selector = result . first_child !
946+ expect ( selector . is_compound ) . toBe ( false )
947+ } )
948+
949+ test ( 'false when has leading combinator' , ( ) => {
950+ const result = parse_selector ( '> div' )
951+ const selector = result . first_child !
952+ expect ( selector . is_compound ) . toBe ( false )
953+ } )
954+
955+ test ( 'false for wrong type' , ( ) => {
956+ const result = parse_selector ( 'div' )
957+ expect ( result . is_compound ) . toBe ( false ) // NODE_SELECTOR_LIST
958+ } )
959+
960+ test ( 'true for single type selector' , ( ) => {
961+ const result = parse_selector ( 'div' )
962+ const selector = result . first_child !
963+ expect ( selector . is_compound ) . toBe ( true )
964+ } )
965+ } )
966+
967+ describe ( 'first_compound_text property' , ( ) => {
968+ test ( 'returns text before combinator' , ( ) => {
969+ const result = parse_selector ( 'div.foo#bar > p' )
970+ const selector = result . first_child !
971+ expect ( selector . first_compound_text ) . toBe ( 'div.foo#bar' )
972+ } )
973+
974+ test ( 'returns full text when no combinators' , ( ) => {
975+ const result = parse_selector ( 'div.foo#bar' )
976+ const selector = result . first_child !
977+ expect ( selector . first_compound_text ) . toBe ( 'div.foo#bar' )
978+ } )
979+
980+ test ( 'returns empty string for wrong type' , ( ) => {
981+ const result = parse_selector ( 'div' )
982+ expect ( result . first_compound_text ) . toBe ( '' )
983+ } )
984+
985+ test ( 'returns empty string for leading combinator' , ( ) => {
986+ const result = parse_selector ( '> div' )
987+ const selector = result . first_child !
988+ expect ( selector . first_compound_text ) . toBe ( '' )
989+ } )
990+
991+ test ( 'handles complex selectors' , ( ) => {
992+ const result = parse_selector ( 'input[type="text"]:focus::placeholder + label' )
993+ const selector = result . first_child !
994+ expect ( selector . first_compound_text ) . toBe ( 'input[type="text"]:focus::placeholder' )
995+ } )
996+ } )
997+
998+ describe ( 'edge cases' , ( ) => {
999+ test ( 'handles :host(#foo.bar baz) nested selector' , ( ) => {
1000+ const result = parse_selector ( ':host(#foo.bar baz)' )
1001+ const selector = result . first_child
1002+ expect ( selector ) . not . toBeNull ( )
1003+ const pseudo = selector ! . first_child
1004+ const innerList = pseudo ?. selector_list
1005+ const innerSel = innerList ?. first_child
1006+
1007+ const compound = innerSel ?. first_compound
1008+ expect ( compound ?. length ) . toBe ( 2 )
1009+ expect ( compound ?. [ 0 ] ?. text ) . toBe ( '#foo' )
1010+ expect ( compound ?. [ 1 ] ?. text ) . toBe ( '.bar' )
1011+ } )
1012+
1013+ test ( 'handles empty selector' , ( ) => {
1014+ const result = parse_selector ( '' )
1015+ const selector = result . first_child
1016+ if ( selector ) {
1017+ expect ( selector . first_compound ) . toEqual ( [ ] )
1018+ expect ( selector . all_compounds ) . toEqual ( [ ] )
1019+ }
1020+ } )
1021+
1022+ test ( 'handles universal selector with combinator' , ( ) => {
1023+ const result = parse_selector ( '* > div' )
1024+ const selector = result . first_child
1025+ expect ( selector ) . not . toBeNull ( )
1026+
1027+ const compounds = selector ! . all_compounds
1028+ expect ( compounds . length ) . toBe ( 2 )
1029+ expect ( compounds [ 0 ] [ 0 ] . text ) . toBe ( '*' )
1030+ expect ( compounds [ 1 ] [ 0 ] . text ) . toBe ( 'div' )
1031+ } )
1032+
1033+ test ( 'handles nesting selector with combinator' , ( ) => {
1034+ const result = parse_selector ( '& > div' )
1035+ const selector = result . first_child !
1036+
1037+ const compounds = selector . all_compounds
1038+ expect ( compounds . length ) . toBe ( 2 )
1039+ expect ( compounds [ 0 ] [ 0 ] . text ) . toBe ( '&' )
1040+ expect ( compounds [ 1 ] [ 0 ] . text ) . toBe ( 'div' )
1041+ } )
1042+ } )
1043+ } )
7731044} )
0 commit comments