From e18b6bd0d8f246f496a43cae181da10edf10b1d4 Mon Sep 17 00:00:00 2001 From: Brandon Kraft Date: Wed, 22 Apr 2026 09:55:23 -0500 Subject: [PATCH] Expand Markpub parser test coverage (issue #30) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fill the branch-coverage gaps flagged during the Markpub parser review. Adds tests for list handler (ordered/unordered, empty-item skip, inline formatting), quote handler (inner paragraph, multi-line, innerHTML fallback), container handler (group, columns, column), fallback delegation, image-without-img-tag, heading default level, whitespace handling for heading/paragraph, code language fence, code entity decoding, link paren encoding,
hard break, inline entity decoding, inline image, and nested inline formatting. Tightens four existing contract assertions from assertStringContainsString to assertSame so format drift surfaces as a test failure. Extends the atmosphere_html_to_markdown filter test to verify the 2-arg contract. Fixes #30 Stacked on #9 — merge that first. --- .../content-parser/class-test-markpub.php | 345 +++++++++++++++++- 1 file changed, 334 insertions(+), 11 deletions(-) diff --git a/tests/phpunit/tests/content-parser/class-test-markpub.php b/tests/phpunit/tests/content-parser/class-test-markpub.php index 2fa50e5..bf59ea3 100644 --- a/tests/phpunit/tests/content-parser/class-test-markpub.php +++ b/tests/phpunit/tests/content-parser/class-test-markpub.php @@ -82,7 +82,7 @@ public function test_converts_headings() { $content = '

My Heading

'; $result = $this->parser->parse( $content, $post ); - $this->assertStringContainsString( '## My Heading', $result['text']['markdown'] ); + $this->assertSame( '## My Heading', $result['text']['markdown'] ); } /** @@ -93,7 +93,7 @@ public function test_converts_heading_level_3() { $content = '

Sub Heading

'; $result = $this->parser->parse( $content, $post ); - $this->assertStringContainsString( '### Sub Heading', $result['text']['markdown'] ); + $this->assertSame( '### Sub Heading', $result['text']['markdown'] ); } /** @@ -147,10 +147,8 @@ public function test_converts_code_blocks() { $post = self::factory()->post->create_and_get(); $content = '
echo "hello";
'; $result = $this->parser->parse( $content, $post ); - $md = $result['text']['markdown']; - $this->assertStringContainsString( '```', $md ); - $this->assertStringContainsString( 'echo "hello";', $md ); + $this->assertSame( "```\necho \"hello\";\n```", $result['text']['markdown'] ); } /** @@ -174,7 +172,7 @@ public function test_converts_separator() { . '

After

'; $result = $this->parser->parse( $content, $post ); - $this->assertStringContainsString( '---', $result['text']['markdown'] ); + $this->assertSame( "Before\n\n---\n\nAfter", $result['text']['markdown'] ); } /** @@ -188,22 +186,33 @@ public function test_empty_content() { /** * Test the atmosphere_html_to_markdown filter. + * + * Verifies the filter callback receives ($markdown, $content) so + * callers can inspect the raw source alongside the conversion. */ public function test_html_to_markdown_filter() { + $received = array(); + \add_filter( 'atmosphere_html_to_markdown', - static fn() => 'custom markdown', + static function ( $markdown, $content ) use ( &$received ) { + $received = array( + 'markdown' => $markdown, + 'content' => $content, + ); + return 'custom markdown'; + }, 10, 2 ); $post = self::factory()->post->create_and_get(); - $result = $this->parser->parse( - '

Hello

', - $post - ); + $source = '

Hello

'; + $result = $this->parser->parse( $source, $post ); $this->assertSame( 'custom markdown', $result['text']['markdown'] ); + $this->assertSame( 'Hello', $received['markdown'] ); + $this->assertSame( $source, $received['content'] ); \remove_all_filters( 'atmosphere_html_to_markdown' ); } @@ -265,4 +274,318 @@ public function test_parse_returns_null_when_markdown_is_empty() { $this->assertNull( $this->parser->parse( $content, $post ) ); } + + /** + * Test ordered list produces numbered markdown. + */ + public function test_listing_ordered() { + $post = self::factory()->post->create_and_get(); + $content = "\n
    " + . '
  1. First
  2. ' + . '
  3. Second
  4. ' + . '
  5. Third
  6. ' + . "
\n"; + + $result = $this->parser->parse( $content, $post ); + + $this->assertSame( "1. First\n2. Second\n3. Third", $result['text']['markdown'] ); + } + + /** + * Test unordered list produces dashed markdown. + */ + public function test_listing_unordered() { + $post = self::factory()->post->create_and_get(); + $content = "\n\n"; + + $result = $this->parser->parse( $content, $post ); + + $this->assertSame( "- First\n- Second\n- Third", $result['text']['markdown'] ); + } + + /** + * Test ordered list skips empty items without gapping the counter. + */ + public function test_listing_skips_empty_items_without_gap() { + $post = self::factory()->post->create_and_get(); + $content = "\n
    " + . '
  1. First
  2. ' + . '
  3. ' + . '
  4. Third
  5. ' + . "
\n"; + + $result = $this->parser->parse( $content, $post ); + + $this->assertSame( "1. First\n2. Third", $result['text']['markdown'] ); + } + + /** + * Test list items preserve inline formatting. + */ + public function test_listing_preserves_inline_formatting() { + $post = self::factory()->post->create_and_get(); + $content = "\n\n"; + + $result = $this->parser->parse( $content, $post ); + + $this->assertSame( '- some **bold**', $result['text']['markdown'] ); + } + + /** + * Test quote block wraps an inner paragraph in a "> " prefix. + */ + public function test_quote_with_inner_paragraph() { + $post = self::factory()->post->create_and_get(); + $content = '
' + . '

Paragraph text

' + . '
'; + + $result = $this->parser->parse( $content, $post ); + + $this->assertSame( '> Paragraph text', $result['text']['markdown'] ); + } + + /** + * Test quote block prefixes every inner line. + */ + public function test_quote_prefixes_every_line() { + $post = self::factory()->post->create_and_get(); + $content = '
' + . '

First

' + . '

Second

' + . '
'; + + $result = $this->parser->parse( $content, $post ); + + $this->assertSame( "> First\n> Second", $result['text']['markdown'] ); + } + + /** + * Test quote falls back to innerHTML when no innerBlocks are present. + */ + public function test_quote_innerhtml_fallback() { + $post = self::factory()->post->create_and_get(); + $content = '
Direct quote text
'; + + $result = $this->parser->parse( $content, $post ); + + $this->assertSame( '> Direct quote text', $result['text']['markdown'] ); + } + + /** + * Test core/group containers flatten inner block markdown. + */ + public function test_container_group() { + $post = self::factory()->post->create_and_get(); + $content = '
' + . '

Inside group

' + . '
'; + + $result = $this->parser->parse( $content, $post ); + + $this->assertSame( 'Inside group', $result['text']['markdown'] ); + } + + /** + * Test core/columns containers flatten inner block markdown. + */ + public function test_container_columns() { + $post = self::factory()->post->create_and_get(); + $content = '
' + . '

Inside columns

' + . '
'; + + $result = $this->parser->parse( $content, $post ); + + $this->assertSame( 'Inside columns', $result['text']['markdown'] ); + } + + /** + * Test core/column containers flatten inner block markdown. + */ + public function test_container_column() { + $post = self::factory()->post->create_and_get(); + $content = '
' + . '

Inside column

' + . '
'; + + $result = $this->parser->parse( $content, $post ); + + $this->assertSame( 'Inside column', $result['text']['markdown'] ); + } + + /** + * Test fallback delegates to container() when innerBlocks exist. + */ + public function test_fallback_delegates_to_container_with_inner_blocks() { + $post = self::factory()->post->create_and_get(); + $content = '
' + . '

Inside unknown

' + . '
'; + + $result = $this->parser->parse( $content, $post ); + + $this->assertSame( 'Inside unknown', $result['text']['markdown'] ); + } + + /** + * Test image() skips blocks without an tag so surrounding + * content renders with no empty separator. + * + * Uses a mixed fixture so a regression returning "" instead of null + * would produce a leading blank line and fail this exact-match + * assertion (the whole-post empty guard in parse() would otherwise + * mask the handler bug). + */ + public function test_image_without_img_tag_is_skipped_cleanly() { + $post = self::factory()->post->create_and_get(); + $content = '
' + . '
Just a caption
' + . "
\n\n" + . '

After

'; + + $result = $this->parser->parse( $content, $post ); + + $this->assertSame( 'After', $result['text']['markdown'] ); + } + + /** + * Test heading defaults to level 2 when attrs.level is missing. + */ + public function test_heading_defaults_to_level_2() { + $post = self::factory()->post->create_and_get(); + $content = '

Default level

'; + + $result = $this->parser->parse( $content, $post ); + + $this->assertSame( '## Default level', $result['text']['markdown'] ); + } + + /** + * Test whitespace-only heading block is skipped cleanly. + * + * Mixed with a non-empty sibling so a regression returning "" from + * heading() would produce a leading blank line and fail the exact + * assertion (the whole-post empty guard would otherwise hide it). + */ + public function test_heading_whitespace_is_skipped_cleanly() { + $post = self::factory()->post->create_and_get(); + $content = "

\n\n" + . '

After

'; + + $result = $this->parser->parse( $content, $post ); + + $this->assertSame( 'After', $result['text']['markdown'] ); + } + + /** + * Test whitespace-only paragraph block is skipped cleanly. + * + * Mixed with a non-empty sibling so a regression returning "" from + * paragraph() would produce a leading blank line and fail the exact + * assertion (the whole-post empty guard would otherwise hide it). + */ + public function test_paragraph_whitespace_is_skipped_cleanly() { + $post = self::factory()->post->create_and_get(); + $content = "

\n\n" + . '

After

'; + + $result = $this->parser->parse( $content, $post ); + + $this->assertSame( 'After', $result['text']['markdown'] ); + } + + /** + * Test code block emits the configured language in the fence. + */ + public function test_code_emits_language_fence() { + $post = self::factory()->post->create_and_get(); + $content = '
echo 1;
'; + + $result = $this->parser->parse( $content, $post ); + + $this->assertStringStartsWith( "```php\n", $result['text']['markdown'] ); + } + + /** + * Test code block decodes HTML entities inside the fence. + */ + public function test_code_decodes_html_entities() { + $post = self::factory()->post->create_and_get(); + $content = '
<div>
'; + + $result = $this->parser->parse( $content, $post ); + + $this->assertSame( "```\n
\n```", $result['text']['markdown'] ); + } + + /** + * Test link URLs have parentheses percent-encoded to protect markdown syntax. + */ + public function test_link_url_parens_percent_encoded() { + $post = self::factory()->post->create_and_get(); + $content = '

See Foo.

'; + + $result = $this->parser->parse( $content, $post ); + $md = $result['text']['markdown']; + + $this->assertStringContainsString( '%28bar%29', $md ); + $this->assertStringNotContainsString( '(bar)', $md ); + } + + /** + * Test
converts to a markdown hard break (two spaces + newline). + */ + public function test_br_converts_to_hard_break() { + $post = self::factory()->post->create_and_get(); + $content = '

line1
line2

'; + + $result = $this->parser->parse( $content, $post ); + + $this->assertStringContainsString( "line1 \nline2", $result['text']['markdown'] ); + } + + /** + * Test HTML entities are decoded in inline paragraph text. + */ + public function test_inline_html_entities_decoded() { + $post = self::factory()->post->create_and_get(); + $content = '

AT&T’s

'; + + $result = $this->parser->parse( $content, $post ); + $md = $result['text']['markdown']; + + $this->assertStringContainsString( 'AT&T', $md ); + $this->assertStringContainsString( "\xE2\x80\x99", $md ); + } + + /** + * Test inline inside a paragraph converts via inline_html_to_markdown. + */ + public function test_inline_image_inside_paragraph() { + $post = self::factory()->post->create_and_get(); + $content = '

Look x here

'; + + $result = $this->parser->parse( $content, $post ); + + $this->assertSame( 'Look ![x](x.jpg) here', $result['text']['markdown'] ); + } + + /** + * Test nested inline formatting (bold wrapping italic). + */ + public function test_nested_inline_formatting() { + $post = self::factory()->post->create_and_get(); + $content = '

bold italic

'; + + $result = $this->parser->parse( $content, $post ); + + $this->assertSame( '**bold *italic***', $result['text']['markdown'] ); + } }