@@ -242,4 +242,234 @@ public function test_generate_options_override_instance_defaults(): void
242242
243243 $ this ->assertStringNotContainsString ('<a ' , $ output );
244244 }
245+
246+ // --- heading splice must not remove existing paragraphs ---
247+
248+ public function test_heading_splice_does_not_remove_existing_paragraphs (): void
249+ {
250+ // array_splice($output, 0, 0, heading) inserts WITHOUT removing.
251+ // Mutations to deleteCount=-1 or 1 would eat paragraphs.
252+ $ output = (new HtmlContentPlaceholder ())->generate ([
253+ 'paragraphs ' => 2 ,
254+ 'includeHeadings ' => true ,
255+ 'includeLists ' => false ,
256+ 'includeQuotes ' => false ,
257+ 'includeCode ' => false ,
258+ 'includeLinks ' => false ,
259+ ]);
260+
261+ $ this ->assertSame (2 , substr_count ($ output , '<p> ' ));
262+ }
263+
264+ // --- heading position: must be first element ---
265+
266+ public function test_heading_is_first_element_with_multiple_paragraphs (): void
267+ {
268+ // With paragraphs=3 the initial array_splice(output, 0, 0, heading) inserts at index 0.
269+ // Mutations to index -1 or 1 would move it away from first position.
270+ $ output = (new HtmlContentPlaceholder ())->generate ([
271+ 'paragraphs ' => 3 ,
272+ 'includeHeadings ' => true ,
273+ 'includeLists ' => false ,
274+ 'includeQuotes ' => false ,
275+ 'includeCode ' => false ,
276+ 'includeLinks ' => false ,
277+ ]);
278+
279+ $ this ->assertStringStartsWith ('<h2> ' , $ output );
280+ }
281+
282+ // --- heading count: initial splice + (paragraphs-1) extra ---
283+
284+ public function test_exactly_two_headings_with_two_paragraphs (): void
285+ {
286+ // paragraphs=2: initial splice adds 1, insertNewOutput(output, 2-1=1) always adds exactly 1 more → total 2.
287+ // Mutations to (paragraphs - 0) or (paragraphs + 1) could add more;
288+ // mutation to (paragraphs - 2 = 0) would skip extra headings → total 1.
289+ $ output = (new HtmlContentPlaceholder ())->generate ([
290+ 'paragraphs ' => 2 ,
291+ 'includeHeadings ' => true ,
292+ 'includeLists ' => false ,
293+ 'includeQuotes ' => false ,
294+ 'includeCode ' => false ,
295+ 'includeLinks ' => false ,
296+ ]);
297+
298+ $ this ->assertSame (2 , substr_count ($ output , '<h2> ' ));
299+ }
300+
301+ // --- links in plaintext use (url) format, not <a> ---
302+
303+ public function test_links_in_plaintext_use_parenthesis_format (): void
304+ {
305+ $ output = (new HtmlContentPlaceholder ())->generate ([
306+ 'paragraphs ' => 1 ,
307+ 'format ' => HtmlContentPlaceholder::FORMAT_PLAINTEXT ,
308+ 'includeLinks ' => true ,
309+ ]);
310+
311+ $ this ->assertStringNotContainsString ('<a ' , $ output );
312+ $ this ->assertMatchesRegularExpression ('/\(https?:\/\// ' , $ output );
313+ }
314+
315+ // --- list always uses <li> items ---
316+
317+ public function test_list_contains_li_items (): void
318+ {
319+ $ output = (new HtmlContentPlaceholder ())->generate ([
320+ 'paragraphs ' => 1 ,
321+ 'includeLists ' => true ,
322+ 'includeLinks ' => false ,
323+ ]);
324+
325+ $ this ->assertStringContainsString ('<li> ' , $ output );
326+ }
327+
328+ // --- list plaintext uses dash prefix ---
329+
330+ public function test_list_in_plaintext_uses_dash_prefix (): void
331+ {
332+ $ output = (new HtmlContentPlaceholder ())->generate ([
333+ 'paragraphs ' => 1 ,
334+ 'format ' => HtmlContentPlaceholder::FORMAT_PLAINTEXT ,
335+ 'includeLists ' => true ,
336+ 'includeLinks ' => false ,
337+ ]);
338+
339+ $ this ->assertStringNotContainsString ('<li> ' , $ output );
340+ $ this ->assertStringContainsString ('- ' , $ output );
341+ }
342+
343+ // --- quote plaintext uses quotation marks ---
344+
345+ public function test_quote_in_plaintext_uses_quotation_marks (): void
346+ {
347+ $ output = (new HtmlContentPlaceholder ())->generate ([
348+ 'paragraphs ' => 1 ,
349+ 'format ' => HtmlContentPlaceholder::FORMAT_PLAINTEXT ,
350+ 'includeQuotes ' => true ,
351+ 'includeLinks ' => false ,
352+ ]);
353+
354+ $ this ->assertStringNotContainsString ('<blockquote> ' , $ output );
355+ $ this ->assertStringContainsString ('" ' , $ output );
356+ }
357+
358+ // --- code plaintext has no pre/code tags ---
359+
360+ public function test_code_in_plaintext_has_no_tags (): void
361+ {
362+ $ output = (new HtmlContentPlaceholder ())->generate ([
363+ 'paragraphs ' => 1 ,
364+ 'format ' => HtmlContentPlaceholder::FORMAT_PLAINTEXT ,
365+ 'includeCode ' => true ,
366+ 'includeLinks ' => false ,
367+ ]);
368+
369+ $ this ->assertStringNotContainsString ('<pre> ' , $ output );
370+ $ this ->assertStringNotContainsString ('<code> ' , $ output );
371+ }
372+
373+ // --- Link href must contain a valid URL, not just any attribute ---
374+
375+ public function test_link_href_contains_valid_url (): void
376+ {
377+ // Kills UnwrapArrayKeys mutant at line 181: without array_keys(), $phrases contains
378+ // URL values as elements (after shuffle re-indexing). The href would then be empty/null
379+ // rather than the correct URL. This test checks the href contains 'https://'.
380+ $ output = (new HtmlContentPlaceholder ())->generate ([
381+ 'paragraphs ' => 1 ,
382+ 'includeLinks ' => true ,
383+ ]);
384+
385+ $ this ->assertStringContainsString ('href="https:// ' , $ output );
386+ }
387+
388+ // --- Paragraph count with zero paragraphs returns empty ---
389+
390+ public function test_zero_paragraphs_returns_empty_string (): void
391+ {
392+ // Kills IncrementInteger/DecrementInteger on loop condition:
393+ // $i = 0; $i < $totalParagraphs means paragraphs=0 → no iterations.
394+ $ output = (new HtmlContentPlaceholder ())->generate ([
395+ 'paragraphs ' => 0 ,
396+ 'includeHeadings ' => false ,
397+ 'includeLists ' => false ,
398+ 'includeQuotes ' => false ,
399+ 'includeCode ' => false ,
400+ 'includeLinks ' => false ,
401+ ]);
402+
403+ $ this ->assertSame ('' , $ output );
404+ }
405+
406+ // --- insertNewOutput with maxInserts=0 does not add content ---
407+
408+ public function test_include_lists_with_zero_paragraphs_produces_no_list (): void
409+ {
410+ // Kills GreaterThan mutation (< 1 → > 1) at line 139 in insertNewOutput.
411+ // With paragraphs=0, includeLists=true calls insertNewOutput($output, 0, ...).
412+ // With original code: 0 < 1 → return early, no list added.
413+ // With mutation (> 1): 0 > 1 = false → would proceed with random_int(1, 0) → ValueError.
414+ // But since paragraphs=0 means $output=[], insertNewOutput returns [] immediately.
415+ // The plain assertion is that no list element exists.
416+ $ output = (new HtmlContentPlaceholder ())->generate ([
417+ 'paragraphs ' => 0 ,
418+ 'includeLists ' => true ,
419+ 'includeLinks ' => false ,
420+ ]);
421+
422+ $ this ->assertStringNotContainsString ('<ul> ' , $ output );
423+ $ this ->assertStringNotContainsString ('<ol> ' , $ output );
424+ $ this ->assertStringNotContainsString ('- ' , $ output );
425+ }
426+
427+ // --- renderList produces valid HTML list structure ---
428+
429+ public function test_list_has_opening_and_closing_tags (): void
430+ {
431+ // Kills ArrayItemRemoval on $tags = ['ul', 'ol'] (line 206):
432+ // removing 'ol' leaves only ['ul'], but list is still rendered with <ul></ul>.
433+ // This isn't fully distinguishable, but tests the basic structure.
434+ // More importantly this also kills FunctionCallRemoval on array_rand().
435+ $ output = (new HtmlContentPlaceholder ())->generate ([
436+ 'paragraphs ' => 1 ,
437+ 'includeLists ' => true ,
438+ 'includeLinks ' => false ,
439+ ]);
440+
441+ $ this ->assertMatchesRegularExpression ('/<(ul|ol)>.*<\/(ul|ol)>/s ' , $ output );
442+ }
443+
444+ // --- renderList plain text uses dash-space prefix on first item ---
445+
446+ public function test_list_in_plaintext_every_item_has_dash_prefix (): void
447+ {
448+ // Kills ConcatOperandRemoval mutant at line 214: removing the '- ' prefix
449+ // makes the first list item appear WITHOUT a dash. The test checks that
450+ // every line that is a list item starts with "- ".
451+ // Strategy: the plain list is "- item1\n- item2\n..." — split by "\n- " gives
452+ // all items. The whole string must start with "- " (the concatenated prefix).
453+ $ output = (new HtmlContentPlaceholder ())->generate ([
454+ 'paragraphs ' => 1 ,
455+ 'format ' => HtmlContentPlaceholder::FORMAT_PLAINTEXT ,
456+ 'includeLists ' => true ,
457+ 'includeLinks ' => false ,
458+ ]);
459+
460+ // Extract the list block: it's the block that contains "\n- " (the implode separator)
461+ // The combined output may contain paragraphs and list separated by "\n\n"
462+ $ blocks = explode ("\n\n" , $ output );
463+ $ listBlock = null ;
464+ foreach ($ blocks as $ block ) {
465+ if (str_contains ($ block , "\n- " )) {
466+ $ listBlock = $ block ;
467+ break ;
468+ }
469+ }
470+
471+ $ this ->assertNotNull ($ listBlock , 'List block must be found in output ' );
472+ // The list block must start with "- " (the prefix on the first item)
473+ $ this ->assertStringStartsWith ('- ' , $ listBlock );
474+ }
245475}
0 commit comments