@@ -240,4 +240,212 @@ public function testFluentInterface()
240240
241241 $ this ->assertInstanceOf (ProgressiveJsonStreamer::class, $ result );
242242 }
243+
244+ public function testActualStreamingBehavior ()
245+ {
246+ $ streamer = new ProgressiveJsonStreamer ();
247+ $ streamer ->data ([
248+ 'immediate ' => '{$} ' ,
249+ 'delayed ' => '{$} ' ,
250+ ]);
251+
252+ $ executionOrder = [];
253+
254+ $ streamer ->addPlaceholder ('immediate ' , function () use (&$ executionOrder ) {
255+ $ executionOrder [] = 'immediate_start ' ;
256+ // Simulate some work
257+ usleep (10000 ); // 10ms
258+ $ executionOrder [] = 'immediate_end ' ;
259+ return 'immediate_value ' ;
260+ });
261+
262+ $ streamer ->addPlaceholder ('delayed ' , function () use (&$ executionOrder ) {
263+ $ executionOrder [] = 'delayed_start ' ;
264+ // Simulate expensive operation
265+ usleep (20000 ); // 20ms
266+ $ executionOrder [] = 'delayed_end ' ;
267+ return 'delayed_value ' ;
268+ });
269+
270+ $ streamChunks = [];
271+ $ chunkTimes = [];
272+
273+ foreach ($ streamer ->stream () as $ chunk ) {
274+ $ streamChunks [] = $ chunk ;
275+ $ chunkTimes [] = microtime (true );
276+ }
277+
278+ // Verify streaming behavior
279+ $ this ->assertCount (3 , $ streamChunks );
280+ $ this ->assertCount (3 , $ chunkTimes );
281+
282+ // Verify execution order (lazy evaluation)
283+ $ this ->assertEquals ([
284+ 'immediate_start ' ,
285+ 'immediate_end ' ,
286+ 'delayed_start ' ,
287+ 'delayed_end '
288+ ], $ executionOrder );
289+
290+ // Verify that chunks are delivered with time gaps
291+ $ this ->assertGreaterThan ($ chunkTimes [0 ], $ chunkTimes [1 ]);
292+ $ this ->assertGreaterThan ($ chunkTimes [1 ], $ chunkTimes [2 ]);
293+ } public function testStreamedResponseOutput ()
294+ {
295+ $ streamer = new ProgressiveJsonStreamer ();
296+ $ streamer ->data (['message ' => '{$} ' ]);
297+ $ streamer ->addPlaceholder ('message ' , fn () => 'Hello Streaming ' );
298+
299+ $ response = $ streamer ->asResponse ();
300+
301+ // Test that the response is a StreamedResponse
302+ $ this ->assertInstanceOf (\Symfony \Component \HttpFoundation \StreamedResponse::class, $ response );
303+ $ this ->assertEquals (200 , $ response ->getStatusCode ());
304+
305+ // Test headers - use get() method which should exist in all versions
306+ $ contentType = $ response ->headers ->get ('Content-Type ' );
307+ $ this ->assertEquals ('application/x-json-stream ' , $ contentType );
308+
309+ $ cacheControl = $ response ->headers ->get ('Cache-Control ' );
310+ $ this ->assertStringContainsString ('no-cache ' , $ cacheControl );
311+ } public function testSendMethodWithOutputBuffering ()
312+ {
313+ $ streamer = new ProgressiveJsonStreamer ();
314+ $ streamer ->data ([
315+ 'step1 ' => '{$} ' ,
316+ 'step2 ' => '{$} ' ,
317+ ]);
318+
319+ $ streamer ->addPlaceholder ('step1 ' , fn () => 'first ' );
320+ $ streamer ->addPlaceholder ('step2 ' , fn () => 'second ' );
321+
322+ // Instead of testing send() directly (which manipulates buffers),
323+ // test that the stream() method works and generates the expected output
324+ $ chunks = [];
325+ foreach ($ streamer ->stream () as $ chunk ) {
326+ $ chunks [] = $ chunk ;
327+ }
328+
329+ // Verify we get the expected number of chunks
330+ $ this ->assertCount (3 , $ chunks ); // Initial structure + 2 placeholders
331+
332+ // Verify the content structure
333+ $ this ->assertStringContainsString ('$step1 ' , $ chunks [0 ]);
334+ $ this ->assertStringContainsString ('$step2 ' , $ chunks [0 ]);
335+ $ this ->assertStringContainsString ('first ' , $ chunks [1 ]);
336+ $ this ->assertStringContainsString ('second ' , $ chunks [2 ]);
337+ $ this ->assertStringContainsString ('/* $step1 */ ' , $ chunks [1 ]);
338+ $ this ->assertStringContainsString ('/* $step2 */ ' , $ chunks [2 ]);
339+ }
340+
341+ public function testSendMethodHeaders ()
342+ {
343+ $ streamer = new ProgressiveJsonStreamer ();
344+ $ streamer ->data (['test ' => '{$} ' ]);
345+ $ streamer ->addPlaceholder ('test ' , fn () => 'value ' );
346+
347+ // Test that the headers method works correctly (used by send())
348+ $ reflection = new \ReflectionClass ($ streamer );
349+ $ method = $ reflection ->getMethod ('getStreamingHeaders ' );
350+ $ method ->setAccessible (true );
351+
352+ $ headers = $ method ->invoke ($ streamer );
353+
354+ // Verify streaming headers are properly configured
355+ $ this ->assertArrayHasKey ('Content-Type ' , $ headers );
356+ $ this ->assertEquals ('application/x-json-stream ' , $ headers ['Content-Type ' ]);
357+ $ this ->assertArrayHasKey ('Cache-Control ' , $ headers );
358+ $ this ->assertStringContainsString ('no-cache ' , $ headers ['Cache-Control ' ]);
359+ $ this ->assertArrayHasKey ('Connection ' , $ headers );
360+ $ this ->assertEquals ('keep-alive ' , $ headers ['Connection ' ]);
361+ }
362+
363+ public function testProgressiveDataAvailability ()
364+ {
365+ $ streamer = new ProgressiveJsonStreamer ();
366+ $ streamer ->data ([
367+ 'user ' => '{$} ' ,
368+ 'posts ' => '{$} ' ,
369+ 'comments ' => '{$} ' ,
370+ ]);
371+
372+ $ dataAvailability = [];
373+
374+ $ streamer ->addPlaceholder ('user ' , function () use (&$ dataAvailability ) {
375+ $ dataAvailability [] = 'user_resolved ' ;
376+ return ['id ' => 1 , 'name ' => 'John ' ];
377+ });
378+
379+ $ streamer ->addPlaceholder ('posts ' , function () use (&$ dataAvailability ) {
380+ $ dataAvailability [] = 'posts_resolved ' ;
381+ return [['title ' => 'Post 1 ' ], ['title ' => 'Post 2 ' ]];
382+ });
383+
384+ $ streamer ->addPlaceholder ('comments ' , function () use (&$ dataAvailability ) {
385+ $ dataAvailability [] = 'comments_resolved ' ;
386+ return [['text ' => 'Comment 1 ' ]];
387+ });
388+
389+ $ chunks = [];
390+ foreach ($ streamer ->stream () as $ chunk ) {
391+ $ chunks [] = $ chunk ;
392+ }
393+
394+ // Verify data is resolved in order and progressively
395+ $ this ->assertEquals ([
396+ 'user_resolved ' ,
397+ 'posts_resolved ' ,
398+ 'comments_resolved '
399+ ], $ dataAvailability );
400+
401+ // Initial structure should be available immediately
402+ $ this ->assertStringContainsString ('$user ' , $ chunks [0 ]);
403+ $ this ->assertStringContainsString ('$posts ' , $ chunks [0 ]);
404+ $ this ->assertStringContainsString ('$comments ' , $ chunks [0 ]);
405+
406+ // Then individual data chunks
407+ $ this ->assertStringContainsString ('John ' , $ chunks [1 ]);
408+ $ this ->assertStringContainsString ('Post 1 ' , $ chunks [2 ]);
409+ $ this ->assertStringContainsString ('Comment 1 ' , $ chunks [3 ]);
410+ }
411+
412+ public function testStreamingWithRealTimeConstraints ()
413+ {
414+ $ streamer = new ProgressiveJsonStreamer ();
415+ $ streamer ->data ([
416+ 'fast ' => '{$} ' ,
417+ 'slow ' => '{$} ' ,
418+ ]);
419+
420+ $ timestamps = [];
421+
422+ $ streamer ->addPlaceholder ('fast ' , function () use (&$ timestamps ) {
423+ $ timestamps ['fast ' ] = microtime (true );
424+ return 'fast_data ' ;
425+ });
426+
427+ $ streamer ->addPlaceholder ('slow ' , function () use (&$ timestamps ) {
428+ usleep (50000 ); // 50ms delay
429+ $ timestamps ['slow ' ] = microtime (true );
430+ return 'slow_data ' ;
431+ });
432+
433+ $ streamStart = microtime (true );
434+ $ chunks = [];
435+
436+ foreach ($ streamer ->stream () as $ chunk ) {
437+ $ chunks [] = $ chunk ;
438+ }
439+
440+ $ streamEnd = microtime (true );
441+
442+ // Verify that the slow placeholder actually caused a delay
443+ $ this ->assertGreaterThan ($ streamStart + 0.04 , $ streamEnd ); // At least 40ms
444+
445+ // Verify fast was resolved before slow
446+ $ this ->assertLessThan ($ timestamps ['slow ' ], $ timestamps ['fast ' ]);
447+
448+ // Verify we got the expected chunks
449+ $ this ->assertCount (3 , $ chunks ); // Initial + fast + slow
450+ }
243451}
0 commit comments