Back to Repositories

Testing Markdown Parser Token-to-HTML Transformation in marked.js

This test suite validates the Parser component of the marked.js library, focusing on both block-level and inline markdown parsing functionality. The tests ensure accurate transformation of markdown tokens into corresponding HTML output.

Test Coverage Overview

The test suite provides comprehensive coverage of Parser functionality including:

  • Block-level elements: paragraphs, headings, code blocks, tables, blockquotes, and lists
  • Inline elements: links, images, text formatting (strong, em, del)
  • Special cases: task lists, loose lists, HTML rendering
  • Token-to-HTML transformation accuracy

Implementation Analysis

The testing approach uses Jest’s describe/it structure with async/await patterns for validation. The expectHtml helper function provides a consistent way to verify token parsing against expected HTML output.

Key implementation features include:
  • Separate testing of block and inline parsing modes
  • Structured token input validation
  • HTML equality checking with detailed diff reporting

Technical Details

Testing tools and setup:

  • Jest test framework with node:test implementation
  • Custom htmlIsEqual and firstDiff utilities from @markedjs/testutils
  • ES modules for Parser implementation
  • Async test execution for all cases

Best Practices Demonstrated

The test suite exemplifies several testing best practices:

  • Hierarchical test organization with logical grouping
  • Consistent test patterns and helper function usage
  • Detailed error reporting with expected vs actual output
  • Comprehensive edge case coverage
  • Clean separation of block and inline parsing tests

markedjs/marked

test/unit/Parser.test.js

            
import { Parser } from '../../lib/marked.esm.js';
import { htmlIsEqual, firstDiff } from '@markedjs/testutils';
import assert from 'node:assert';
import { describe, it } from 'node:test';

async function expectHtml({ tokens, options, html, inline }) {
  const parser = new Parser(options);
  const actual = parser[inline ? 'parseInline' : 'parse'](tokens);
  const testDiff = await firstDiff(actual, html);
  assert.ok(await htmlIsEqual(html, actual), `Expected: ${testDiff.expected}\n  Actual: ${testDiff.actual}`);
}

describe('Parser', () => {
  describe('block', () => {
    it('space between paragraphs', async() => {
      await expectHtml({
        tokens: [
          {
            type: 'paragraph',
            text: 'paragraph 1',
            tokens: [{ type: 'text', text: 'paragraph 1' }],
          },
          { type: 'space' },
          {
            type: 'paragraph',
            text: 'paragraph 2',
            tokens: [{ type: 'text', text: 'paragraph 2' }],
          },
        ],
        html: '<p>paragraph 1</p><p>paragraph 2</p>',
      });
    });

    it('hr', async() => {
      await expectHtml({
        tokens: [
          {
            type: 'hr',
          },
        ],
        html: '<hr />',
      });
    });

    it('heading', async() => {
      await expectHtml({
        tokens: [
          {
            type: 'heading',
            depth: 1,
            text: 'heading',
            tokens: [{ type: 'text', text: 'heading' }],
          },
        ],
        html: '<h1>heading</h1>',
      });
    });

    it('code', async() => {
      await expectHtml({
        tokens: [
          {
            type: 'code',
            text: 'code',
          },
        ],
        html: '<pre><code>code</code></pre>',
      });
    });

    it('table', async() => {
      await expectHtml({
        tokens: [
          {
            type: 'table',
            align: ['left', 'right'],
            header: [
              {
                text: 'a',
                tokens: [{ type: 'text', raw: 'a', text: 'a' }],
                header: true,
                align: 'left',
              },
              {
                text: 'b',
                tokens: [{ type: 'text', raw: 'b', text: 'b' }],
                header: true,
                align: 'right',
              },
            ],
            rows: [
              [
                {
                  text: '1',
                  tokens: [{ type: 'text', raw: '1', text: '1' }],
                  header: false,
                  align: 'left',
                },
                {
                  text: '2',
                  tokens: [{ type: 'text', raw: '2', text: '2' }],
                  header: false,
                  align: 'right',
                },
              ],
            ],
          },
        ],
        html: `
<table>
  <thead>
    <tr>
      <th align="left">a</th>
      <th align="right">b</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td align="left">1</td>
      <td align="right">2</td>
    </tr>
  </tbody>
</table>`,
      });
    });

    it('blockquote', async() => {
      await expectHtml({
        tokens: [
          {
            type: 'blockquote',
            tokens: [
              {
                type: 'paragraph',
                text: 'blockquote',
                tokens: [{ type: 'text', text: 'blockquote' }],
              },
            ],
          },
        ],
        html: '<blockquote><p>blockquote</p></blockquote>',
      });
    });

    describe('list', () => {
      it('unordered', async() => {
        await expectHtml({
          tokens: [
            {
              type: 'list',
              ordered: false,
              start: '',
              loose: false,
              items: [
                {
                  task: false,
                  checked: undefined,
                  tokens: [
                    {
                      type: 'text',
                      text: 'item 1',
                      tokens: [{ type: 'text', text: 'item 1' }],
                    },
                  ],
                },
                {
                  task: false,
                  checked: undefined,
                  tokens: [
                    {
                      type: 'text',
                      text: 'item 2',
                      tokens: [{ type: 'text', text: 'item 2' }],
                    },
                  ],
                },
              ],
            },
          ],
          html: `
<ul>
  <li>item 1</li>
  <li>item 2</li>
</ul>`,
        });
      });

      it('ordered', async() => {
        await expectHtml({
          tokens: [
            {
              type: 'list',
              ordered: true,
              start: 2,
              loose: false,
              items: [
                {
                  task: false,
                  checked: undefined,
                  tokens: [
                    {
                      type: 'text',
                      text: 'item 1',
                      tokens: [{ type: 'text', text: 'item 1' }],
                    },
                  ],
                },
                {
                  task: false,
                  checked: undefined,
                  tokens: [
                    {
                      type: 'text',
                      text: 'item 2',
                      tokens: [{ type: 'text', text: 'item 2' }],
                    },
                  ],
                },
              ],
            },
          ],
          html: `
<ol start='2'>
  <li>item 1</li>
  <li>item 2</li>
</ol>`,
        });
      });

      it('tasks', async() => {
        await expectHtml({
          tokens: [
            {
              type: 'list',
              ordered: false,
              start: '',
              loose: false,
              items: [
                {
                  task: true,
                  checked: false,
                  tokens: [
                    {
                      type: 'text',
                      text: 'item 1',
                      tokens: [{ type: 'text', text: 'item 1' }],
                    },
                  ],
                },
                {
                  task: true,
                  checked: true,
                  tokens: [
                    {
                      type: 'text',
                      text: 'item 2',
                      tokens: [{ type: 'text', text: 'item 2' }],
                    },
                  ],
                },
              ],
            },
          ],
          html: `
<ul>
  <li><input disabled type="checkbox"> item 1</li>
  <li><input checked disabled type="checkbox"> item 2</li>
</ul>`,
        });
      });

      it('loose', async() => {
        await expectHtml({
          tokens: [
            {
              type: 'list',
              ordered: false,
              start: '',
              loose: true,
              items: [
                {
                  task: false,
                  checked: undefined,
                  loose: true,
                  tokens: [
                    {
                      type: 'text',
                      text: 'item 1',
                      tokens: [{ type: 'text', text: 'item 1' }],
                    },
                  ],
                },
                {
                  task: false,
                  checked: undefined,
                  loose: true,
                  tokens: [
                    {
                      type: 'text',
                      text: 'item 2',
                      tokens: [{ type: 'text', text: 'item 2' }],
                    },
                  ],
                },
              ],
            },
          ],
          html: `
  <ul>
    <li><p>item 1</p></li>
    <li><p>item 2</p></li>
  </ul>`,
        });
      });
    });

    it('html', async() => {
      await expectHtml({
        tokens: [
          {
            type: 'html',
            text: '<div>html</div>',
          },
        ],
        html: '<div>html</div>',
      });
    });

    it('paragraph', async() => {
      await expectHtml({
        tokens: [
          {
            type: 'paragraph',
            text: 'paragraph 1',
            tokens: [{ type: 'text', text: 'paragraph 1' }],
          },
        ],
        html: '<p>paragraph 1</p>',
      });
    });

    it('text', async() => {
      await expectHtml({
        tokens: [
          { type: 'text', text: 'text 1' },
          { type: 'text', text: 'text 2' },
        ],
        html: '<p>text 1\ntext 2</p>',
      });
    });
  });

  describe('inline', () => {
    it('escape', async() => {
      await expectHtml({
        inline: true,
        tokens: [{ type: 'escape', text: '>' }],
        html: '>',
      });
    });

    it('html', async() => {
      await expectHtml({
        inline: true,
        tokens: [
          { type: 'html', text: '<div>' },
          { type: 'text', text: 'html' },
          { type: 'html', text: '</div>' },
        ],
        html: '<div>html</div>',
      });
    });

    it('link', async() => {
      await expectHtml({
        inline: true,
        tokens: [
          {
            type: 'link',
            text: 'link',
            href: 'https://example.com',
            title: 'title',
            tokens: [{ type: 'text', text: 'link' }],
          },
        ],
        html: '<a href="https://example.com" title="title">link</a>',
      });
    });

    it('image', async() => {
      await expectHtml({
        inline: true,
        tokens: [
          {
            type: 'image',
            text: 'image',
            href: 'image.png',
            title: 'title',
          },
        ],
        html: '<img src="image.png" alt="image" title="title">',
      });
    });

    it('strong', async() => {
      await expectHtml({
        inline: true,
        tokens: [
          {
            type: 'strong',
            text: 'strong',
            tokens: [{ type: 'text', text: 'strong' }],
          },
        ],
        html: '<strong>strong</strong>',
      });
    });

    it('em', async() => {
      await expectHtml({
        inline: true,
        tokens: [
          {
            type: 'em',
            text: 'em',
            tokens: [{ type: 'text', text: 'em' }],
          },
        ],
        html: '<em>em</em>',
      });
    });

    it('codespan', async() => {
      await expectHtml({
        inline: true,
        tokens: [
          {
            type: 'codespan',
            text: 'code',
          },
        ],
        html: '<code>code</code>',
      });
    });

    it('br', async() => {
      await expectHtml({
        inline: true,
        tokens: [
          {
            type: 'br',
          },
        ],
        html: '<br />',
      });
    });

    it('del', async() => {
      await expectHtml({
        inline: true,
        tokens: [
          {
            type: 'del',
            text: 'del',
            tokens: [{ type: 'text', text: 'del' }],
          },
        ],
        html: '<del>del</del>',
      });
    });

    it('text', async() => {
      await expectHtml({
        inline: true,
        tokens: [
          { type: 'text', text: 'text 1' },
          { type: 'text', text: 'text 2' },
        ],
        html: 'text 1text 2',
      });
    });
  });
});