<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[57822] trunk/tests/phpunit/tests/interactivity-api: Interactivity API: Ensure proper directive processing on special elements.</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { white-space: pre-line; overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/57822">57822</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/57822","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>dmsnell</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2024-03-12 18:52:20 +0000 (Tue, 12 Mar 2024)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>Interactivity API: Ensure proper directive processing on special elements.

Adds a test to ensure proper processing of directives on special HTML elements,
or HTML which contains special elements. These special elements are defined by
the HTML API and are the HTML elements which cannot contain other tags, such as
the IFRAME, SCRIPT, TEXTAREA, TITLE, elements, etc...

The server diretive processor performs a custom tracking of HTML structure and
this test ensures it isn't mislead by the handling of those special elements.

Developed in https://github.com/WordPress/wordpress-develop/pull/6247
Discussed in https://core.trac.wordpress.org/ticket/60746

Props santosguillamot, cbravobernal, mukesh27, westonruter, swissspidy, dmsnell.
Follow-up to <a href="https://core.trac.wordpress.org/changeset/57348">[57348]</a>.
Fixes <a href="https://core.trac.wordpress.org/ticket/60746">#60746</a>.</pre>

<h3>Added Paths</h3>
<ul>
<li><a href="#trunktestsphpunittestsinteractivityapiwpInteractivityAPIFunctionsphp">trunk/tests/phpunit/tests/interactivity-api/wpInteractivityAPIFunctions.php</a></li>
</ul>

<h3>Removed Paths</h3>
<ul>
<li><a href="#trunktestsphpunittestsinteractivityapiinteractivityapiphp">trunk/tests/phpunit/tests/interactivity-api/interactivity-api.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunktestsphpunittestsinteractivityapiinteractivityapiphp"></a>
<div class="delfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Deleted: trunk/tests/phpunit/tests/interactivity-api/interactivity-api.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/interactivity-api/interactivity-api.php 2024-03-12 17:15:40 UTC (rev 57821)
+++ trunk/tests/phpunit/tests/interactivity-api/interactivity-api.php   2024-03-12 18:52:20 UTC (rev 57822)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1,393 +0,0 @@
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-<?php
-/**
- * Unit tests covering the functionality of the public functions of the
- * Interactivity API.
- *
- * @package WordPress
- * @subpackage Interactivity API
- *
- * @since 6.5.0
- *
- * @group interactivity-api
- */
-class Tests_Interactivity_API_Functions extends WP_UnitTestCase {
-       /**
-        * Set up.
-        */
-       public function set_up() {
-               parent::set_up();
-
-               $interactive_block = array(
-                       'render_callback' => function ( $attributes, $content ) {
-                               return '
-                                               <div
-                                                       data-wp-interactive=\'{ "namespace": "myPlugin" }\'
-                                                       data-wp-context=\'{ "block": ' . $attributes['block'] . ' }\'
-                                               >
-                                                       <input
-                                                               class="interactive/block-' . $attributes['block'] . '"
-                                                               data-wp-bind--value="context.block"
-                                                       >' .
-                                               $content .
-                                       '</div>';
-                       },
-                       'supports'        => array(
-                               'interactivity' => true,
-                       ),
-               );
-
-               register_block_type( 'test/interactive-block', $interactive_block );
-               register_block_type( 'test/interactive-block-2', $interactive_block );
-
-               register_block_type(
-                       'test/non-interactive-block',
-                       array(
-                               'render_callback' => function ( $attributes, $content ) {
-                                       $directive = isset( $attributes['hasDirective'] ) ? ' data-wp-bind--value="context.block"' : '';
-                                       return '
-                                               <div>
-                                                       <input class="non-interactive/block-' . $attributes['block'] . '"' . $directive . '>' .
-                                                       $content .
-                                               '</div>';
-                               },
-                       )
-               );
-       }
-
-       /**
-        * Tear down.
-        */
-       public function tear_down() {
-               unregister_block_type( 'test/interactive-block' );
-               unregister_block_type( 'test/interactive-block-2' );
-               unregister_block_type( 'test/non-interactive-block' );
-               parent::tear_down();
-       }
-
-       /**
-        * Tests processing of a single interactive block.
-        *
-        * @ticket 60356
-        *
-        * @covers ::wp_interactivity_process_directives_of_interactive_blocks
-        */
-       public function test_processs_directives_of_single_interactive_block() {
-               $post_content    = '<!-- wp:test/interactive-block { "block": 1 } /-->';
-               $rendered_blocks = do_blocks( $post_content );
-               $p               = new WP_HTML_Tag_Processor( $rendered_blocks );
-               $p->next_tag( array( 'class_name' => 'interactive/block-1' ) );
-               $this->assertEquals( '1', $p->get_attribute( 'value' ) );
-       }
-
-       /**
-        * Tests processing of multiple interactive blocks in parallel along with a
-        * non-interactive block.
-        *
-        * @ticket 60356
-        *
-        * @covers ::wp_interactivity_process_directives_of_interactive_blocks
-        */
-       public function test_processs_directives_of_multiple_interactive_blocks_in_paralell() {
-               $post_content    = '
-                       <!-- wp:test/interactive-block { "block": 1 } /-->
-                       <!-- wp:test/interactive-block-2 { "block": 2 } /-->
-                       <!-- wp:test/non-interactive-block { "block": 3, "hasDirective": true } /-->
-                       <!-- wp:test/interactive-block { "block": 4 } /-->
-               ';
-               $rendered_blocks = do_blocks( $post_content );
-               $p               = new WP_HTML_Tag_Processor( $rendered_blocks );
-               $p->next_tag( array( 'class_name' => 'interactive/block-1' ) );
-               $this->assertEquals( '1', $p->get_attribute( 'value' ) );
-               $p->next_tag( array( 'class_name' => 'interactive/block-2' ) );
-               $this->assertEquals( '2', $p->get_attribute( 'value' ) );
-               $p->next_tag( array( 'class_name' => 'non-interactive/block-3' ) );
-               $this->assertNull( $p->get_attribute( 'value' ) );
-               $p->next_tag( array( 'class_name' => 'interactive/block-4' ) );
-               $this->assertEquals( '4', $p->get_attribute( 'value' ) );
-       }
-
-       /**
-        * Tests processing of an interactive block inside a non-interactive block.
-        *
-        * @ticket 60356
-        *
-        * @covers ::wp_interactivity_process_directives_of_interactive_blocks
-        */
-       public function test_processs_directives_of_interactive_block_inside_non_interactive_block() {
-               $post_content    = '
-                       <!-- wp:test/non-interactive-block { "block": 1 } -->
-                               <!-- wp:test/interactive-block { "block": 2 } /-->
-                       <!-- /wp:test/non-interactive-block -->
-               ';
-               $rendered_blocks = do_blocks( $post_content );
-               $p               = new WP_HTML_Tag_Processor( $rendered_blocks );
-               $p->next_tag( array( 'class_name' => 'interactive/block-2' ) );
-               $this->assertEquals( '2', $p->get_attribute( 'value' ) );
-       }
-
-       /**
-        * Tests processing of multiple interactive blocks nested inside a
-        * non-interactive block.
-        *
-        * @ticket 60356
-        *
-        * @covers ::wp_interactivity_process_directives_of_interactive_blocks
-        */
-       public function test_processs_directives_of_multple_interactive_blocks_inside_non_interactive_block() {
-               $post_content    = '
-                       <!-- wp:test/non-interactive-block { "block": 1 } -->
-                               <!-- wp:test/interactive-block { "block": 2 } /-->
-                               <!-- wp:test/interactive-block { "block": 3 } /-->
-                       <!-- /wp:test/non-interactive-block -->
-               ';
-               $rendered_blocks = do_blocks( $post_content );
-               $p               = new WP_HTML_Tag_Processor( $rendered_blocks );
-               $p->next_tag( array( 'class_name' => 'interactive/block-2' ) );
-               $this->assertEquals( '2', $p->get_attribute( 'value' ) );
-               $p->next_tag( array( 'class_name' => 'interactive/block-3' ) );
-               $this->assertEquals( '3', $p->get_attribute( 'value' ) );
-       }
-
-       /**
-        * Tests processing of a single interactive block directive nested inside
-        * multiple non-interactive blocks.
-        *
-        * @ticket 60356
-        *
-        * @covers ::wp_interactivity_process_directives_of_interactive_blocks
-        */
-       public function test_processs_directives_of_interactive_block_inside_multple_non_interactive_block() {
-               $post_content    = '
-                       <!-- wp:test/non-interactive-block { "block": 1 } -->
-                               <!-- wp:test/interactive-block { "block": 2 } /-->
-                       <!-- /wp:test/non-interactive-block -->
-                       <!-- wp:test/non-interactive-block { "block": 3 } -->
-                               <!-- wp:test/interactive-block-2 { "block": 4 } /-->
-                       <!-- /wp:test/non-interactive-block -->
-               ';
-               $rendered_blocks = do_blocks( $post_content );
-               $p               = new WP_HTML_Tag_Processor( $rendered_blocks );
-               $p->next_tag( array( 'class_name' => 'interactive/block-2' ) );
-               $this->assertEquals( '2', $p->get_attribute( 'value' ) );
-               $p->next_tag( array( 'class_name' => 'interactive/block-4' ) );
-               $this->assertEquals( '4', $p->get_attribute( 'value' ) );
-       }
-
-       /**
-        * Tests processing of directives for an interactive block containing a
-        * non-interactive block without directives.
-        *
-        * @ticket 60356
-        *
-        * @covers ::wp_interactivity_process_directives_of_interactive_blocks
-        */
-       public function test_processs_directives_of_interactive_block_containing_non_interactive_block_without_directives() {
-               $post_content    = '
-                       <!-- wp:test/interactive-block { "block": 1 } -->
-                               <!-- wp:test/non-interactive-block { "block": 2 } /-->
-                       <!-- /wp:test/interactive-block -->
-               ';
-               $rendered_blocks = do_blocks( $post_content );
-               $p               = new WP_HTML_Tag_Processor( $rendered_blocks );
-               $p->next_tag( array( 'class_name' => 'interactive/block-1' ) );
-               $this->assertEquals( '1', $p->get_attribute( 'value' ) );
-               $p->next_tag( array( 'class_name' => 'non-interactive/block-2' ) );
-               $this->assertNull( $p->get_attribute( 'value' ) );
-       }
-
-       /**
-        * Tests processing of directives for an interactive block containing a
-        * non-interactive block with directives.
-        *
-        * @ticket 60356
-        *
-        * @covers ::wp_interactivity_process_directives_of_interactive_blocks
-        */
-       public function test_processs_directives_of_interactive_block_containing_non_interactive_block_with_directives() {
-               $post_content    = '
-                       <!-- wp:test/interactive-block { "block": 1 } -->
-                               <!-- wp:test/non-interactive-block { "block": 2, "hasDirective": true } /-->
-                       <!-- /wp:test/interactive-block -->
-               ';
-               $rendered_blocks = do_blocks( $post_content );
-               $p               = new WP_HTML_Tag_Processor( $rendered_blocks );
-               $p->next_tag( array( 'class_name' => 'interactive/block-1' ) );
-               $this->assertEquals( '1', $p->get_attribute( 'value' ) );
-               $p->next_tag( array( 'class_name' => 'non-interactive/block-2' ) );
-               $this->assertEquals( '1', $p->get_attribute( 'value' ) );
-       }
-
-       /**
-        * Tests processing of directives for an interactive block containing nested
-        * interactive and non-interactive blocks, checking proper propagation of
-        * context.
-        *
-        * @ticket 60356
-        *
-        * @covers ::wp_interactivity_process_directives_of_interactive_blocks
-        */
-       public function test_processs_directives_of_interactive_block_containing_nested_interactive_and_non_interactive_blocks() {
-               $post_content    = '
-                       <!-- wp:test/interactive-block { "block": 1 } -->
-                               <!-- wp:test/interactive-block-2 { "block": 2 } -->
-                                       <!-- wp:test/non-interactive-block { "block": 3, "hasDirective": true } /-->
-                               <!-- /wp:test/interactive-block-2 -->
-                               <!-- wp:test/non-interactive-block { "block": 4, "hasDirective": true } /-->
-                       <!-- /wp:test/interactive-block -->
-               ';
-               $rendered_blocks = do_blocks( $post_content );
-               $p               = new WP_HTML_Tag_Processor( $rendered_blocks );
-               $p->next_tag( array( 'class_name' => 'interactive/block-1' ) );
-               $this->assertEquals( '1', $p->get_attribute( 'value' ) );
-               $p->next_tag( array( 'class_name' => 'interactive/block-2' ) );
-               $this->assertEquals( '2', $p->get_attribute( 'value' ) );
-               $p->next_tag( array( 'class_name' => 'non-interactive/block-3' ) );
-               $this->assertEquals( '2', $p->get_attribute( 'value' ) );
-               $p->next_tag( array( 'class_name' => 'non-interactive/block-4' ) );
-               $this->assertEquals( '1', $p->get_attribute( 'value' ) );
-       }
-
-       /**
-        * Counter for the number of times the test directive processor is called.
-        *
-        * @var int
-        */
-       private $data_wp_test_processor_count = 0;
-
-       /**
-        * Test directive processor callback.
-        *
-        * Increments the $data_wp_test_processor_count every time a tag that is not a
-        * tag closer is processed.
-        *
-        * @param WP_HTML_Tag_Processor $p Instance of the processor handling the current HTML tag.
-        */
-       public function data_wp_test_processor( $p ) {
-               if ( ! $p->is_tag_closer() ) {
-                       $this->data_wp_test_processor_count = $this->data_wp_test_processor_count + 1;
-               }
-       }
-
-       /**
-        * Tests that directives are only processed once for the root interactive
-        * blocks.
-        *
-        * This ensures that nested blocks do not trigger additional processing of the
-        * same directives, leading to incorrect behavior or performance issues.
-        *
-        * @ticket 60356
-        *
-        * @covers ::wp_interactivity_process_directives_of_interactive_blocks
-        */
-       public function test_process_directives_only_process_the_root_interactive_blocks() {
-               $class                = new ReflectionClass( 'WP_Interactivity_API' );
-               $directive_processors = $class->getProperty( 'directive_processors' );
-               $directive_processors->setAccessible( true );
-               $old_directive_processors = $directive_processors->getValue();
-               $directive_processors->setValue( null, array( 'data-wp-test' => array( $this, 'data_wp_test_processor' ) ) );
-               $html                               = '<div data-wp-test></div>';
-               $this->data_wp_test_processor_count = 0;
-               wp_interactivity_process_directives( $html );
-               $this->assertEquals( 1, $this->data_wp_test_processor_count );
-
-               register_block_type(
-                       'test/custom-directive-block',
-                       array(
-                               'render_callback' => function ( $attributes, $content ) {
-                                       return '<div class="test" data-wp-test>' . $content . '</div>';
-                               },
-                               'supports'        => array(
-                                       'interactivity' => true,
-                               ),
-                       )
-               );
-               $post_content                       = '
-                       <!-- wp:test/custom-directive-block -->
-                               <!-- wp:test/custom-directive-block /-->
-                       <!-- /wp:test/custom-directive-block -->
-               ';
-               $this->data_wp_test_processor_count = 0;
-               do_blocks( $post_content );
-               $this->assertEquals( 2, $this->data_wp_test_processor_count );
-               unregister_block_type( 'test/custom-directive-block' );
-               $directive_processors->setValue( null, $old_directive_processors );
-       }
-
-       /**
-        * Tests that wp_interactivity_data_wp_context function correctly converts different array
-        * structures to a JSON string.
-        *
-        * @ticket 60356
-        *
-        * @covers ::wp_interactivity_data_wp_context
-        */
-       public function test_wp_interactivity_data_wp_context_with_different_arrays() {
-               $this->assertEquals( 'data-wp-context=\'{}\'', wp_interactivity_data_wp_context( array() ) );
-               $this->assertEquals(
-                       'data-wp-context=\'{"a":1,"b":"2","c":true}\'',
-                       wp_interactivity_data_wp_context(
-                               array(
-                                       'a' => 1,
-                                       'b' => '2',
-                                       'c' => true,
-                               )
-                       )
-               );
-               $this->assertEquals(
-                       'data-wp-context=\'{"a":[1,2]}\'',
-                       wp_interactivity_data_wp_context( array( 'a' => array( 1, 2 ) ) )
-               );
-               $this->assertEquals(
-                       'data-wp-context=\'[1,2]\'',
-                       wp_interactivity_data_wp_context( array( 1, 2 ) )
-               );
-       }
-
-       /**
-        * Tests that wp_interactivity_data_wp_context function correctly converts different array
-        * structures to a JSON string and adds a namespace.
-        *
-        * @ticket 60356
-        *
-        * @covers ::wp_interactivity_data_wp_context
-        */
-       public function test_wp_interactivity_data_wp_context_with_different_arrays_and_a_namespace() {
-               $this->assertEquals( 'data-wp-context=\'myPlugin::{}\'', wp_interactivity_data_wp_context( array(), 'myPlugin' ) );
-               $this->assertEquals(
-                       'data-wp-context=\'myPlugin::{"a":1,"b":"2","c":true}\'',
-                       wp_interactivity_data_wp_context(
-                               array(
-                                       'a' => 1,
-                                       'b' => '2',
-                                       'c' => true,
-                               ),
-                               'myPlugin'
-                       )
-               );
-               $this->assertEquals(
-                       'data-wp-context=\'myPlugin::{"a":[1,2]}\'',
-                       wp_interactivity_data_wp_context( array( 'a' => array( 1, 2 ) ), 'myPlugin' )
-               );
-               $this->assertEquals(
-                       'data-wp-context=\'myPlugin::[1,2]\'',
-                       wp_interactivity_data_wp_context( array( 1, 2 ), 'myPlugin' )
-               );
-       }
-
-       /**
-        * Tests that wp_interactivity_data_wp_context function correctly applies the JSON encoding
-        * flags. This ensures that characters like `<`, `>`, `'`, or `&` are
-        * properly escaped in the JSON-encoded string to prevent potential XSS
-        * attacks.
-        *
-        * @ticket 60356
-        *
-        * @covers ::wp_interactivity_data_wp_context
-        */
-       public function test_wp_interactivity_data_wp_context_with_json_flags() {
-               $this->assertEquals( 'data-wp-context=\'{"tag":"\u003Cfoo\u003E"}\'', wp_interactivity_data_wp_context( array( 'tag' => '<foo>' ) ) );
-               $this->assertEquals( 'data-wp-context=\'{"apos":"\u0027bar\u0027"}\'', wp_interactivity_data_wp_context( array( 'apos' => "'bar'" ) ) );
-               $this->assertEquals( 'data-wp-context=\'{"quot":"\u0022baz\u0022"}\'', wp_interactivity_data_wp_context( array( 'quot' => '"baz"' ) ) );
-               $this->assertEquals( 'data-wp-context=\'{"amp":"T\u0026T"}\'', wp_interactivity_data_wp_context( array( 'amp' => 'T&T' ) ) );
-       }
-}
</del></span></pre></div>
<a id="trunktestsphpunittestsinteractivityapiwpInteractivityAPIFunctionsphpfromrev57821trunktestsphpunittestsinteractivityapiinteractivityapiphp"></a>
<div class="copfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Copied: trunk/tests/phpunit/tests/interactivity-api/wpInteractivityAPIFunctions.php (from rev 57821, trunk/tests/phpunit/tests/interactivity-api/interactivity-api.php)</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/interactivity-api/wpInteractivityAPIFunctions.php                               (rev 0)
+++ trunk/tests/phpunit/tests/interactivity-api/wpInteractivityAPIFunctions.php 2024-03-12 18:52:20 UTC (rev 57822)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,420 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Unit tests covering the functionality of the public functions of the
+ * Interactivity API.
+ *
+ * @package WordPress
+ * @subpackage Interactivity API
+ *
+ * @since 6.5.0
+ *
+ * @group interactivity-api
+ */
+class Tests_Interactivity_API_wpInteractivityAPIFunctions extends WP_UnitTestCase {
+       /**
+        * Set up.
+        */
+       public function set_up() {
+               parent::set_up();
+
+               $interactive_block = array(
+                       'render_callback' => function ( $attributes, $content ) {
+                               return '
+                                               <div
+                                                       data-wp-interactive=\'{ "namespace": "myPlugin" }\'
+                                                       data-wp-context=\'{ "block": ' . $attributes['block'] . ' }\'
+                                               >
+                                                       <input
+                                                               class="interactive/block-' . $attributes['block'] . '"
+                                                               data-wp-bind--value="context.block"
+                                                       >' .
+                                               $content .
+                                       '</div>';
+                       },
+                       'supports'        => array(
+                               'interactivity' => true,
+                       ),
+               );
+
+               register_block_type( 'test/interactive-block', $interactive_block );
+               register_block_type( 'test/interactive-block-2', $interactive_block );
+
+               register_block_type(
+                       'test/non-interactive-block',
+                       array(
+                               'render_callback' => function ( $attributes, $content ) {
+                                       $directive = isset( $attributes['hasDirective'] ) ? ' data-wp-bind--value="context.block"' : '';
+                                       return '
+                                               <div>
+                                                       <input class="non-interactive/block-' . $attributes['block'] . '"' . $directive . '>' .
+                                                       $content .
+                                               '</div>';
+                               },
+                       )
+               );
+       }
+
+       /**
+        * Tear down.
+        */
+       public function tear_down() {
+               unregister_block_type( 'test/interactive-block' );
+               unregister_block_type( 'test/interactive-block-2' );
+               unregister_block_type( 'test/non-interactive-block' );
+               parent::tear_down();
+       }
+
+       /**
+        * Tests processing of a single interactive block.
+        *
+        * @ticket 60356
+        *
+        * @covers ::wp_interactivity_process_directives_of_interactive_blocks
+        */
+       public function test_processs_directives_of_single_interactive_block() {
+               $post_content    = '<!-- wp:test/interactive-block { "block": 1 } /-->';
+               $rendered_blocks = do_blocks( $post_content );
+               $p               = new WP_HTML_Tag_Processor( $rendered_blocks );
+               $p->next_tag( array( 'class_name' => 'interactive/block-1' ) );
+               $this->assertEquals( '1', $p->get_attribute( 'value' ) );
+       }
+
+       /**
+        * Tests processing of multiple interactive blocks in parallel along with a
+        * non-interactive block.
+        *
+        * @ticket 60356
+        *
+        * @covers ::wp_interactivity_process_directives_of_interactive_blocks
+        */
+       public function test_processs_directives_of_multiple_interactive_blocks_in_paralell() {
+               $post_content    = '
+                       <!-- wp:test/interactive-block { "block": 1 } /-->
+                       <!-- wp:test/interactive-block-2 { "block": 2 } /-->
+                       <!-- wp:test/non-interactive-block { "block": 3, "hasDirective": true } /-->
+                       <!-- wp:test/interactive-block { "block": 4 } /-->
+               ';
+               $rendered_blocks = do_blocks( $post_content );
+               $p               = new WP_HTML_Tag_Processor( $rendered_blocks );
+               $p->next_tag( array( 'class_name' => 'interactive/block-1' ) );
+               $this->assertEquals( '1', $p->get_attribute( 'value' ) );
+               $p->next_tag( array( 'class_name' => 'interactive/block-2' ) );
+               $this->assertEquals( '2', $p->get_attribute( 'value' ) );
+               $p->next_tag( array( 'class_name' => 'non-interactive/block-3' ) );
+               $this->assertNull( $p->get_attribute( 'value' ) );
+               $p->next_tag( array( 'class_name' => 'interactive/block-4' ) );
+               $this->assertEquals( '4', $p->get_attribute( 'value' ) );
+       }
+
+       /**
+        * Tests processing of an interactive block inside a non-interactive block.
+        *
+        * @ticket 60356
+        *
+        * @covers ::wp_interactivity_process_directives_of_interactive_blocks
+        */
+       public function test_processs_directives_of_interactive_block_inside_non_interactive_block() {
+               $post_content    = '
+                       <!-- wp:test/non-interactive-block { "block": 1 } -->
+                               <!-- wp:test/interactive-block { "block": 2 } /-->
+                       <!-- /wp:test/non-interactive-block -->
+               ';
+               $rendered_blocks = do_blocks( $post_content );
+               $p               = new WP_HTML_Tag_Processor( $rendered_blocks );
+               $p->next_tag( array( 'class_name' => 'interactive/block-2' ) );
+               $this->assertEquals( '2', $p->get_attribute( 'value' ) );
+       }
+
+       /**
+        * Tests processing of multiple interactive blocks nested inside a
+        * non-interactive block.
+        *
+        * @ticket 60356
+        *
+        * @covers ::wp_interactivity_process_directives_of_interactive_blocks
+        */
+       public function test_processs_directives_of_multple_interactive_blocks_inside_non_interactive_block() {
+               $post_content    = '
+                       <!-- wp:test/non-interactive-block { "block": 1 } -->
+                               <!-- wp:test/interactive-block { "block": 2 } /-->
+                               <!-- wp:test/interactive-block { "block": 3 } /-->
+                       <!-- /wp:test/non-interactive-block -->
+               ';
+               $rendered_blocks = do_blocks( $post_content );
+               $p               = new WP_HTML_Tag_Processor( $rendered_blocks );
+               $p->next_tag( array( 'class_name' => 'interactive/block-2' ) );
+               $this->assertEquals( '2', $p->get_attribute( 'value' ) );
+               $p->next_tag( array( 'class_name' => 'interactive/block-3' ) );
+               $this->assertEquals( '3', $p->get_attribute( 'value' ) );
+       }
+
+       /**
+        * Tests processing of a single interactive block directive nested inside
+        * multiple non-interactive blocks.
+        *
+        * @ticket 60356
+        *
+        * @covers ::wp_interactivity_process_directives_of_interactive_blocks
+        */
+       public function test_processs_directives_of_interactive_block_inside_multple_non_interactive_block() {
+               $post_content    = '
+                       <!-- wp:test/non-interactive-block { "block": 1 } -->
+                               <!-- wp:test/interactive-block { "block": 2 } /-->
+                       <!-- /wp:test/non-interactive-block -->
+                       <!-- wp:test/non-interactive-block { "block": 3 } -->
+                               <!-- wp:test/interactive-block-2 { "block": 4 } /-->
+                       <!-- /wp:test/non-interactive-block -->
+               ';
+               $rendered_blocks = do_blocks( $post_content );
+               $p               = new WP_HTML_Tag_Processor( $rendered_blocks );
+               $p->next_tag( array( 'class_name' => 'interactive/block-2' ) );
+               $this->assertEquals( '2', $p->get_attribute( 'value' ) );
+               $p->next_tag( array( 'class_name' => 'interactive/block-4' ) );
+               $this->assertEquals( '4', $p->get_attribute( 'value' ) );
+       }
+
+       /**
+        * Tests processing of directives for an interactive block containing a
+        * non-interactive block without directives.
+        *
+        * @ticket 60356
+        *
+        * @covers ::wp_interactivity_process_directives_of_interactive_blocks
+        */
+       public function test_processs_directives_of_interactive_block_containing_non_interactive_block_without_directives() {
+               $post_content    = '
+                       <!-- wp:test/interactive-block { "block": 1 } -->
+                               <!-- wp:test/non-interactive-block { "block": 2 } /-->
+                       <!-- /wp:test/interactive-block -->
+               ';
+               $rendered_blocks = do_blocks( $post_content );
+               $p               = new WP_HTML_Tag_Processor( $rendered_blocks );
+               $p->next_tag( array( 'class_name' => 'interactive/block-1' ) );
+               $this->assertEquals( '1', $p->get_attribute( 'value' ) );
+               $p->next_tag( array( 'class_name' => 'non-interactive/block-2' ) );
+               $this->assertNull( $p->get_attribute( 'value' ) );
+       }
+
+       /**
+        * Tests processing of directives for an interactive block containing a
+        * non-interactive block with directives.
+        *
+        * @ticket 60356
+        *
+        * @covers ::wp_interactivity_process_directives_of_interactive_blocks
+        */
+       public function test_processs_directives_of_interactive_block_containing_non_interactive_block_with_directives() {
+               $post_content    = '
+                       <!-- wp:test/interactive-block { "block": 1 } -->
+                               <!-- wp:test/non-interactive-block { "block": 2, "hasDirective": true } /-->
+                       <!-- /wp:test/interactive-block -->
+               ';
+               $rendered_blocks = do_blocks( $post_content );
+               $p               = new WP_HTML_Tag_Processor( $rendered_blocks );
+               $p->next_tag( array( 'class_name' => 'interactive/block-1' ) );
+               $this->assertEquals( '1', $p->get_attribute( 'value' ) );
+               $p->next_tag( array( 'class_name' => 'non-interactive/block-2' ) );
+               $this->assertEquals( '1', $p->get_attribute( 'value' ) );
+       }
+
+       /**
+        * Tests processing of directives for an interactive block containing nested
+        * interactive and non-interactive blocks, checking proper propagation of
+        * context.
+        *
+        * @ticket 60356
+        *
+        * @covers ::wp_interactivity_process_directives_of_interactive_blocks
+        */
+       public function test_processs_directives_of_interactive_block_containing_nested_interactive_and_non_interactive_blocks() {
+               $post_content    = '
+                       <!-- wp:test/interactive-block { "block": 1 } -->
+                               <!-- wp:test/interactive-block-2 { "block": 2 } -->
+                                       <!-- wp:test/non-interactive-block { "block": 3, "hasDirective": true } /-->
+                               <!-- /wp:test/interactive-block-2 -->
+                               <!-- wp:test/non-interactive-block { "block": 4, "hasDirective": true } /-->
+                       <!-- /wp:test/interactive-block -->
+               ';
+               $rendered_blocks = do_blocks( $post_content );
+               $p               = new WP_HTML_Tag_Processor( $rendered_blocks );
+               $p->next_tag( array( 'class_name' => 'interactive/block-1' ) );
+               $this->assertEquals( '1', $p->get_attribute( 'value' ) );
+               $p->next_tag( array( 'class_name' => 'interactive/block-2' ) );
+               $this->assertEquals( '2', $p->get_attribute( 'value' ) );
+               $p->next_tag( array( 'class_name' => 'non-interactive/block-3' ) );
+               $this->assertEquals( '2', $p->get_attribute( 'value' ) );
+               $p->next_tag( array( 'class_name' => 'non-interactive/block-4' ) );
+               $this->assertEquals( '1', $p->get_attribute( 'value' ) );
+       }
+
+       /**
+        * Counter for the number of times the test directive processor is called.
+        *
+        * @var int
+        */
+       private $data_wp_test_processor_count = 0;
+
+       /**
+        * Test directive processor callback.
+        *
+        * Increments the $data_wp_test_processor_count every time a tag that is not a
+        * tag closer is processed.
+        *
+        * @param WP_HTML_Tag_Processor $p Instance of the processor handling the current HTML tag.
+        */
+       public function data_wp_test_processor( $p ) {
+               if ( ! $p->is_tag_closer() ) {
+                       $this->data_wp_test_processor_count = $this->data_wp_test_processor_count + 1;
+               }
+       }
+
+       /**
+        * Tests that directives are only processed once for the root interactive
+        * blocks.
+        *
+        * This ensures that nested blocks do not trigger additional processing of the
+        * same directives, leading to incorrect behavior or performance issues.
+        *
+        * @ticket 60356
+        *
+        * @covers ::wp_interactivity_process_directives_of_interactive_blocks
+        */
+       public function test_process_directives_only_process_the_root_interactive_blocks() {
+               $class                = new ReflectionClass( 'WP_Interactivity_API' );
+               $directive_processors = $class->getProperty( 'directive_processors' );
+               $directive_processors->setAccessible( true );
+               $old_directive_processors = $directive_processors->getValue();
+               $directive_processors->setValue( null, array( 'data-wp-test' => array( $this, 'data_wp_test_processor' ) ) );
+               $html                               = '<div data-wp-test></div>';
+               $this->data_wp_test_processor_count = 0;
+               wp_interactivity_process_directives( $html );
+               $this->assertEquals( 1, $this->data_wp_test_processor_count );
+
+               register_block_type(
+                       'test/custom-directive-block',
+                       array(
+                               'render_callback' => function ( $attributes, $content ) {
+                                       return '<div class="test" data-wp-test>' . $content . '</div>';
+                               },
+                               'supports'        => array(
+                                       'interactivity' => true,
+                               ),
+                       )
+               );
+               $post_content                       = '
+                       <!-- wp:test/custom-directive-block -->
+                               <!-- wp:test/custom-directive-block /-->
+                       <!-- /wp:test/custom-directive-block -->
+               ';
+               $this->data_wp_test_processor_count = 0;
+               do_blocks( $post_content );
+               $this->assertEquals( 2, $this->data_wp_test_processor_count );
+               unregister_block_type( 'test/custom-directive-block' );
+               $directive_processors->setValue( null, $old_directive_processors );
+       }
+
+       /**
+        * Tests that wp_interactivity_data_wp_context function correctly converts different array
+        * structures to a JSON string.
+        *
+        * @ticket 60356
+        *
+        * @covers ::wp_interactivity_data_wp_context
+        */
+       public function test_wp_interactivity_data_wp_context_with_different_arrays() {
+               $this->assertEquals( 'data-wp-context=\'{}\'', wp_interactivity_data_wp_context( array() ) );
+               $this->assertEquals(
+                       'data-wp-context=\'{"a":1,"b":"2","c":true}\'',
+                       wp_interactivity_data_wp_context(
+                               array(
+                                       'a' => 1,
+                                       'b' => '2',
+                                       'c' => true,
+                               )
+                       )
+               );
+               $this->assertEquals(
+                       'data-wp-context=\'{"a":[1,2]}\'',
+                       wp_interactivity_data_wp_context( array( 'a' => array( 1, 2 ) ) )
+               );
+               $this->assertEquals(
+                       'data-wp-context=\'[1,2]\'',
+                       wp_interactivity_data_wp_context( array( 1, 2 ) )
+               );
+       }
+
+       /**
+        * Tests that wp_interactivity_data_wp_context function correctly converts different array
+        * structures to a JSON string and adds a namespace.
+        *
+        * @ticket 60356
+        *
+        * @covers ::wp_interactivity_data_wp_context
+        */
+       public function test_wp_interactivity_data_wp_context_with_different_arrays_and_a_namespace() {
+               $this->assertEquals( 'data-wp-context=\'myPlugin::{}\'', wp_interactivity_data_wp_context( array(), 'myPlugin' ) );
+               $this->assertEquals(
+                       'data-wp-context=\'myPlugin::{"a":1,"b":"2","c":true}\'',
+                       wp_interactivity_data_wp_context(
+                               array(
+                                       'a' => 1,
+                                       'b' => '2',
+                                       'c' => true,
+                               ),
+                               'myPlugin'
+                       )
+               );
+               $this->assertEquals(
+                       'data-wp-context=\'myPlugin::{"a":[1,2]}\'',
+                       wp_interactivity_data_wp_context( array( 'a' => array( 1, 2 ) ), 'myPlugin' )
+               );
+               $this->assertEquals(
+                       'data-wp-context=\'myPlugin::[1,2]\'',
+                       wp_interactivity_data_wp_context( array( 1, 2 ), 'myPlugin' )
+               );
+       }
+
+       /**
+        * Tests that wp_interactivity_data_wp_context function correctly applies the JSON encoding
+        * flags. This ensures that characters like `<`, `>`, `'`, or `&` are
+        * properly escaped in the JSON-encoded string to prevent potential XSS
+        * attacks.
+        *
+        * @ticket 60356
+        *
+        * @covers ::wp_interactivity_data_wp_context
+        */
+       public function test_wp_interactivity_data_wp_context_with_json_flags() {
+               $this->assertEquals( 'data-wp-context=\'{"tag":"\u003Cfoo\u003E"}\'', wp_interactivity_data_wp_context( array( 'tag' => '<foo>' ) ) );
+               $this->assertEquals( 'data-wp-context=\'{"apos":"\u0027bar\u0027"}\'', wp_interactivity_data_wp_context( array( 'apos' => "'bar'" ) ) );
+               $this->assertEquals( 'data-wp-context=\'{"quot":"\u0022baz\u0022"}\'', wp_interactivity_data_wp_context( array( 'quot' => '"baz"' ) ) );
+               $this->assertEquals( 'data-wp-context=\'{"amp":"T\u0026T"}\'', wp_interactivity_data_wp_context( array( 'amp' => 'T&T' ) ) );
+       }
+
+       /**
+        * Tests that directives processing of tags that don't visit closer tag work.
+        *
+        * @ticket 60746
+        *
+        * @covers ::wp_interactivity_process_directives_of_interactive_blocks
+        */
+       public function test_process_directives_in_tags_that_dont_visit_closer_tag() {
+               register_block_type(
+                       'test/custom-directive-block',
+                       array(
+                               'render_callback' => function () {
+                                       return '<iframe data-wp-interactive="nameSpace" ' . wp_interactivity_data_wp_context( array( 'text' => 'test' ) ) . ' data-wp-class--test="context.text" src="1"></iframe>';
+                               },
+                               'supports'        => array(
+                                       'interactivity' => true,
+                               ),
+                       )
+               );
+               $post_content      = '<!-- wp:test/custom-directive-block /-->';
+               $processed_content = do_blocks( $post_content );
+               $processor         = new WP_HTML_Tag_Processor( $processed_content );
+               $processor->next_tag( array( 'class_name' => 'test' ) );
+               unregister_block_type( 'test/custom-directive-block' );
+               $this->assertEquals( '1', $processor->get_attribute( 'src' ) );
+       }
+}
</ins></span></pre>
</div>
</div>

</body>
</html>