@@ -645,6 +645,185 @@ public function test_build_state_selector_splits_selector_lists_without_splittin
645645 );
646646 }
647647
648+ /**
649+ * Tests that a leading compound naming a different block is kept as an
650+ * ancestor, and the instance class goes on the block's own compound.
651+ *
652+ * @covers ::wp_build_state_selector
653+ * @covers ::wp_get_state_selector_ancestor
654+ *
655+ * @ticket 66252
656+ *
657+ * @dataProvider data_build_state_selector_with_block_name
658+ *
659+ * @param string $block_name Block name.
660+ * @param string $block_selector Block or feature selector.
661+ * @param string $expected Expected scoped selector.
662+ */
663+ public function test_build_state_selector_with_block_name ( $ block_name , $ block_selector , $ expected ) {
664+ $ this ->assertSame (
665+ $ expected ,
666+ wp_build_state_selector ( '.wp-states-test ' , $ block_selector , ':hover ' , $ block_name )
667+ );
668+ }
669+
670+ /**
671+ * Data provider for test_build_state_selector_with_block_name().
672+ *
673+ * @return array<string, array<string, string>>
674+ */
675+ public function data_build_state_selector_with_block_name () {
676+ return array (
677+ 'own class ' => array (
678+ 'block_name ' => 'core/paragraph ' ,
679+ 'block_selector ' => '.wp-block-paragraph ' ,
680+ 'expected ' => '.wp-states-test:hover ' ,
681+ ),
682+ 'tag name ' => array (
683+ 'block_name ' => 'core/paragraph ' ,
684+ 'block_selector ' => 'p ' ,
685+ 'expected ' => '.wp-states-test:hover ' ,
686+ ),
687+ 'own class with modifier ' => array (
688+ 'block_name ' => 'core/x ' ,
689+ 'block_selector ' => '.wp-block-x.is-style-y ' ,
690+ 'expected ' => '.wp-states-test.is-style-y:hover ' ,
691+ ),
692+ 'own class with descendant ' => array (
693+ 'block_name ' => 'core/search ' ,
694+ 'block_selector ' => '.wp-block-search .wp-block-search__input ' ,
695+ 'expected ' => '.wp-states-test .wp-block-search__input:hover ' ,
696+ ),
697+ 'ancestor block with child combinator ' => array (
698+ 'block_name ' => 'core/list-item ' ,
699+ 'block_selector ' => '.wp-block-list > li ' ,
700+ 'expected ' => '.wp-block-list > .wp-states-test:hover ' ,
701+ ),
702+ 'ancestor block with :not() ' => array (
703+ 'block_name ' => 'core/list-item ' ,
704+ 'block_selector ' => '.wp-block-list:not(.wp-block-list .wp-block-list) > li ' ,
705+ 'expected ' => '.wp-block-list:not(.wp-block-list .wp-block-list) > .wp-states-test:hover ' ,
706+ ),
707+ 'ancestor block with element descendant ' => array (
708+ 'block_name ' => 'core/list-item ' ,
709+ 'block_selector ' => '.wp-block-list > li a ' ,
710+ 'expected ' => '.wp-block-list > .wp-states-test a:hover ' ,
711+ ),
712+ 'other block with nested block element ' => array (
713+ 'block_name ' => 'test/state-button ' ,
714+ 'block_selector ' => '.wp-block-button .wp-block-button__link ' ,
715+ 'expected ' => '.wp-states-test .wp-block-button__link:hover ' ,
716+ ),
717+ 'selector list ' => array (
718+ 'block_name ' => 'core/list-item ' ,
719+ 'block_selector ' => '.wp-block-list > li, .wp-block-list-item .inner ' ,
720+ 'expected ' => '.wp-block-list > .wp-states-test:hover, .wp-states-test .inner:hover ' ,
721+ ),
722+ );
723+ }
724+
725+ /**
726+ * Tests that the selectors registered by core blocks scope to their own element.
727+ *
728+ * The cases above pass selectors as literals, so a change to a block's
729+ * `block.json` would not reach them. This one reads what the block actually
730+ * registers, and fails if a selector changes into a shape the scoping does
731+ * not handle.
732+ *
733+ * @covers ::wp_build_state_selector
734+ * @covers ::wp_get_state_selector_ancestor
735+ *
736+ * @ticket 66252
737+ *
738+ * @dataProvider data_registered_block_selectors_scope_to_their_own_element
739+ *
740+ * @param string $block_name Block name.
741+ * @param string $feature Key in the block's `selectors` metadata.
742+ */
743+ public function test_registered_block_selectors_scope_to_their_own_element ( $ block_name , $ feature ) {
744+ $ block_type = WP_Block_Type_Registry::get_instance ()->get_registered ( $ block_name );
745+
746+ if ( ! $ block_type || empty ( $ block_type ->selectors [ $ feature ] ) ) {
747+ $ this ->markTestSkipped ( "$ block_name does not register a ' $ feature' selector. " );
748+ }
749+
750+ $ scoped = wp_build_state_selector (
751+ '.wp-states-test ' ,
752+ $ block_type ->selectors [ $ feature ],
753+ ':hover ' ,
754+ $ block_name
755+ );
756+
757+ /*
758+ * The instance class sits on the block's own element, so the state
759+ * selector has to end on that class rather than describe a descendant
760+ * of it.
761+ */
762+ $ this ->assertStringEndsWith (
763+ '.wp-states-test:hover ' ,
764+ $ scoped ,
765+ "The scoped ' $ feature' selector for $ block_name does not land on the block's own element. "
766+ );
767+ }
768+
769+ /**
770+ * Data provider for test_registered_block_selectors_scope_to_their_own_element().
771+ *
772+ * @return array<string, array<string, string>>
773+ */
774+ public function data_registered_block_selectors_scope_to_their_own_element () {
775+ // Only selectors that target the block's own element belong here. A
776+ // feature selector for a descendant, such as a link inside the block,
777+ // ends on that descendant by design.
778+ return array (
779+ 'list item root ' => array (
780+ 'block_name ' => 'core/list-item ' ,
781+ 'feature ' => 'root ' ,
782+ ),
783+ 'list item border ' => array (
784+ 'block_name ' => 'core/list-item ' ,
785+ 'feature ' => 'border ' ,
786+ ),
787+ );
788+ }
789+
790+ /**
791+ * Tests that a List Item responsive value is scoped to the `li` itself.
792+ *
793+ * @covers ::wp_render_block_states_support
794+ *
795+ * @ticket 66252
796+ */
797+ public function test_responsive_state_scopes_ancestor_root_selector_to_block_wrapper () {
798+ $ this ->ensure_block_registered (
799+ 'core/list-item ' ,
800+ array ( 'root ' => '.wp-block-list > li ' )
801+ );
802+
803+ $ block = array (
804+ 'blockName ' => 'core/list-item ' ,
805+ 'attrs ' => array (
806+ 'style ' => array (
807+ '@mobile ' => array (
808+ 'color ' => array (
809+ 'background ' => '#ff00d0 ' ,
810+ ),
811+ ),
812+ ),
813+ ),
814+ );
815+
816+ $ actual = wp_render_block_states_support ( '<li>Item</li> ' , $ block );
817+
818+ preg_match ( '/wp-states-[a-f0-9]{8}/ ' , $ actual , $ matches );
819+ $ actual_stylesheet = wp_style_engine_get_stylesheet_from_context ( 'block-supports ' , array ( 'prettify ' => false ) );
820+
821+ $ this ->assertStringContainsString (
822+ '@media (width <= 480px){.wp-block-list > . ' . $ matches [0 ] . '{background-color:#ff00d0 !important;background-image:unset !important;}} ' ,
823+ $ actual_stylesheet
824+ );
825+ }
826+
648827 /**
649828 * Tests that preset values are converted to CSS custom property references.
650829 *
0 commit comments