@@ -182,7 +182,8 @@ private function announced_paths(): array {
182182 /**
183183 * Tests that a token renders as the exact content of the host, however the
184184 * reference reaches it, and reaches the output only there: `data-wp-bind`
185- * does not set it and the client data does not hold it.
185+ * does not set it and the client data holds none of its HTML. A token in
186+ * state prints as the marker `{"wpServerHtml":true}`.
186187 *
187188 * @ticket 66246
188189 *
@@ -191,8 +192,9 @@ private function announced_paths(): array {
191192 * @dataProvider data_ways_to_reach_a_token
192193 *
193194 * @param callable $make_state Callable returning the state to set.
195+ * @param string $printed How `state.html` is expected to print in the client data.
194196 */
195- public function test_token_reaches_the_output_only_as_host_content ( callable $ make_state ) {
197+ public function test_token_reaches_the_output_only_as_host_content ( callable $ make_state, string $ printed ) {
196198 $ this ->setExpectedIncorrectUsage ( 'WP_Interactivity_API::data_wp_bind_processor ' );
197199 $ this ->set_state ( $ make_state () );
198200
@@ -220,22 +222,22 @@ public function test_token_reaches_the_output_only_as_host_content( callable $ma
220222 'Expected only the non-scalar notice of each bound attribute. '
221223 );
222224
223- $ printed = $ this ->printed_client_data ();
224- $ this ->assertStringNotContainsString ( 'marker-7f3a ' , $ printed , 'Expected the client data to hold no trusted HTML. ' );
225- $ this ->assertStringContainsString ( '"html":{} ' , $ printed , 'Expected the token to print as an empty object. ' );
225+ $ this ->assertStringNotContainsString ( 'marker-7f3a ' , $ this ->printed_client_data (), 'Expected the client data to hold no trusted HTML. ' );
226+ $ this ->assertStringContainsString ( '"html": ' . $ printed , $ this ->printed_client_data (), 'Expected state.html to print as given. ' );
226227 }
227228
228229 /**
229230 * Data provider.
230231 *
231- * @return array<string, array{ 0: callable }> Data provider.
232+ * @return array<string, array{ 0: callable, 1: string }> Data provider.
232233 */
233234 public static function data_ways_to_reach_a_token (): array {
234235 return array (
235236 'token in state ' => array (
236237 static function () {
237238 return array ( 'html ' => wp_interactivity_as_dangerous_html ( self ::MARKER_HTML ) );
238239 },
240+ '{"wpServerHtml":true} ' ,
239241 ),
240242 'closure returning a token ' => array (
241243 static function () {
@@ -245,16 +247,19 @@ static function () {
245247 },
246248 );
247249 },
250+ '{} ' ,
248251 ),
249252 'clone of a token ' => array (
250253 static function () {
251254 return array ( 'html ' => clone wp_interactivity_as_dangerous_html ( self ::MARKER_HTML ) );
252255 },
256+ '{"wpServerHtml":true} ' ,
253257 ),
254258 'token created with new ' => array (
255259 static function () {
256260 return array ( 'html ' => new WP_Interactivity_Dangerous_HTML ( self ::MARKER_HTML ) );
257261 },
262+ '{"wpServerHtml":true} ' ,
258263 ),
259264 );
260265 }
@@ -394,6 +399,7 @@ public static function data_values_that_are_not_tokens(): array {
394399 'json_decode object ' => array ( json_decode ( '{"html":"<p>Hi</p>"} ' ) ),
395400 'other object ' => array ( new ArrayObject ( array ( '<p>Hi</p> ' ) ) ),
396401 'json_decode of a token ' => array ( json_decode ( wp_json_encode ( new WP_Interactivity_Dangerous_HTML ( '<p>Hi</p> ' ) ) ) ),
402+ 'marker decoded as an array ' => array ( json_decode ( '{"wpServerHtml":true} ' , true ) ),
397403 'closure returning a string ' => array (
398404 static function () {
399405 return '<p>Hi</p> ' ;
@@ -729,11 +735,11 @@ public function test_unbalanced_markup_returns_input_unchanged() {
729735 }
730736
731737 /**
732- * Tests that a token restored by unserialization does not render but is
733- * listed like any other token: the host keeps its fallback, exactly one
734- * notice is raised for the unserialization and one for the decline, the
735- * markup after the host is processed, and the token prints as an empty
736- * object. The token that was serialized still renders.
738+ * Tests that a token restored by unserialization does not render but prints
739+ * as the marker like any other token: the host keeps its fallback, exactly
740+ * one notice is raised for the unserialization and one for the decline, and
741+ * the markup after the host is processed. The token that was serialized
742+ * still renders.
737743 *
738744 * @ticket 66246
739745 *
@@ -772,18 +778,18 @@ public function test_unserialized_token_declines_as_not_a_token() {
772778 );
773779 $ this ->assertCount ( 1 , $ this ->html_notice_messages (), 'Expected exactly one data-wp-html notice. ' );
774780 $ this ->assertCount ( 2 , $ this ->notices , 'Expected no notice besides those of the unserialization and the decline. ' );
775- $ this ->assertSame ( array ( 'test ' => array ( 'state.html ' , 'state.original ' ) ), $ this ->announced_paths () );
776781 $ this ->assertEquals (
777- (object ) array (),
782+ (object ) array ( ' wpServerHtml ' => true ),
778783 json_decode ( $ this ->printed_client_data () )->state ->test ->html ,
779- 'Expected the token to print as an empty object . '
784+ 'Expected the token to print as the marker . '
780785 );
781786 }
782787
783788 /**
784- * Tests that a token in `wp_interactivity_data_wp_context()` reaches neither
785- * the output nor the client data, and that referencing it from other
786- * directives neither renders it nor aborts processing.
789+ * Tests that a token in `wp_interactivity_data_wp_context()` prints as the
790+ * marker, so its HTML reaches neither the output nor the client data, and
791+ * that referencing it from other directives neither renders it nor aborts
792+ * processing.
787793 *
788794 * @ticket 66246
789795 *
@@ -809,30 +815,25 @@ public function test_token_in_context_helper_reaches_no_output_or_client_data()
809815 $ output ,
810816 'Expected the context to hold no HTML to render, and the markup after it to be processed. '
811817 );
812- $ this ->assertStringContainsString ( '{"h":{}} ' , $ output , 'Expected the token to print as an empty object in the context attribute. ' );
818+ $ this ->assertStringContainsString ( '{"h":{"wpServerHtml":true }} ' , $ output , 'Expected the token to print as the marker in the context attribute. ' );
813819 $ this ->assertStringNotContainsString ( 'marker-7f3a ' , $ output );
814820 $ this ->assertStringNotContainsString ( 'marker-7f3a ' , $ this ->printed_client_data () );
815821 $ this ->assertCount ( 1 , $ this ->html_notice_messages (), 'Expected the value read from context to decline as not a token. ' );
816822 }
817823
818824 /**
819- * Tests that every host renders its token and that a token reached through
820- * state is listed with the full path of the reference, under the namespace
821- * the reference resolves in, once per path and namespace. A path is listed
822- * without a leading `!`, also when only a directive other than
823- * `data-wp-html` references it.
825+ * Tests that every host renders the token its reference reaches through
826+ * state, at any depth and in any namespace.
824827 *
825828 * @ticket 66246
826829 *
827830 * @covers ::process_directives
828- * @covers ::filter_script_module_interactivity_data
829831 */
830- public function test_token_paths_are_listed_in_full_per_namespace () {
832+ public function test_tokens_render_at_any_depth_and_namespace () {
831833 $ this ->set_state (
832834 array (
833- 'html ' => $ this ->token (),
834- 'deep ' => array ( 'er ' => array ( 'html ' => $ this ->token () ) ),
835- 'negated ' => $ this ->token (),
835+ 'html ' => $ this ->token (),
836+ 'deep ' => array ( 'er ' => array ( 'html ' => $ this ->token () ) ),
836837 )
837838 );
838839 wp_interactivity_state (
@@ -849,18 +850,9 @@ public function test_token_paths_are_listed_in_full_per_namespace() {
849850 $ hosts .= '<div data-wp-html=" ' . $ reference . '">Fallback</div> ' ;
850851 $ rendered .= '<div data-wp-html=" ' . $ reference . '"> ' . self ::MARKER_HTML . '</div> ' ;
851852 }
852- $ negated = ' <div data-wp-class--off="!state.negated">x</div> ' ;
853+ $ output = wp_interactivity_process_directives ( $ this -> page ( $ hosts ) ) ;
853854
854- $ output = wp_interactivity_process_directives ( $ this ->page ( $ hosts . $ negated ) );
855-
856- $ this ->assertSame ( $ this ->processed_page ( $ rendered . $ negated ), $ output , 'Expected every host to render its token. ' );
857- $ this ->assertSame (
858- array (
859- 'test ' => array ( 'state.deep.er.html ' , 'state.html ' , 'state.negated ' ),
860- 'other ' => array ( 'state.html ' , 'state.deep.er.html ' ),
861- ),
862- $ this ->announced_paths ()
863- );
855+ $ this ->assertSame ( $ this ->processed_page ( $ rendered ), $ output , 'Expected every host to render its token. ' );
864856 }
865857
866858 /**
@@ -893,15 +885,15 @@ public function test_token_in_a_closure_result_lists_only_the_closure() {
893885
894886 /**
895887 * Tests that tokens reached through `context`, such as the items of
896- * `data-wp-each`, render without a notice and are not listed, although
897- * they are held in state.
888+ * `data-wp-each`, render without a notice, add no listed path, and print
889+ * as the marker in the state that holds them .
898890 *
899891 * @ticket 66246
900892 *
901893 * @covers ::process_directives
902894 * @covers ::filter_script_module_interactivity_data
903895 */
904- public function test_tokens_reached_through_context_are_not_listed () {
896+ public function test_tokens_reached_through_context_render_and_print_as_the_marker () {
905897 $ this ->set_state (
906898 array (
907899 'items ' => array (
@@ -918,6 +910,40 @@ public function test_tokens_reached_through_context_are_not_listed() {
918910 $ this ->assertSame ( 2 , substr_count ( $ output , '<li data-wp-each-child="test::state.items" data-wp-html="context.item.html"> ' . self ::MARKER_HTML . '</li> ' ) );
919911 $ this ->assertSame ( array (), $ this ->notices , 'Expected no notice. ' );
920912 $ this ->assertSame ( array (), $ this ->announced_paths (), 'Expected no listed path. ' );
913+ $ this ->assertStringContainsString (
914+ '"items":[{"html":{"wpServerHtml":true}},{"html":{"wpServerHtml":true}}] ' ,
915+ $ this ->printed_client_data (),
916+ 'Expected each item token to print as the marker. '
917+ );
918+ }
919+
920+ /**
921+ * Tests that a token prints as the marker on both encoding paths of
922+ * `wp_json_encode()`, including the one taken when the data holds a string
923+ * that is not valid UTF-8, and that iterating a token yields only the
924+ * marker.
925+ *
926+ * @ticket 66246
927+ *
928+ * @covers WP_Interactivity_Dangerous_HTML::jsonSerialize
929+ * @covers WP_Interactivity_Dangerous_HTML::getIterator
930+ */
931+ public function test_token_prints_as_the_marker_on_both_encoding_paths () {
932+ $ token = $ this ->token ();
933+
934+ $ this ->assertSame ( '{"wpServerHtml":true} ' , wp_json_encode ( $ token ) );
935+
936+ $ data = array (
937+ 'bad ' => "caf \xE9" ,
938+ 'token ' => $ token ,
939+ );
940+ $ this ->assertFalse ( json_encode ( $ data ), 'Expected the data to take the fallback path. ' );
941+ $ fallback = wp_json_encode ( $ data );
942+ $ this ->assertIsString ( $ fallback );
943+ $ this ->assertStringContainsString ( '"token":{"wpServerHtml":true} ' , $ fallback , 'Expected the fallback path to print the marker. ' );
944+ $ this ->assertStringNotContainsString ( 'marker-7f3a ' , $ fallback );
945+
946+ $ this ->assertSame ( array ( 'wpServerHtml ' => true ), iterator_to_array ( $ token ) );
921947 }
922948
923949 /**
0 commit comments