Back to Repositories

Testing Hook System Implementation in Marked Parser

This test suite examines the hook functionality in the Marked markdown parser, focusing on preprocessing, postprocessing, and token manipulation capabilities. The tests verify both synchronous and asynchronous hook operations, ensuring robust markdown transformation and HTML generation.

Test Coverage Overview

The test suite provides comprehensive coverage of Marked’s hook system, including:
  • Preprocessing markdown input
  • Postprocessing HTML output
  • Token processing and manipulation
  • Lexer and parser customization
  • Synchronous and asynchronous hook execution

Implementation Analysis

The testing approach utilizes Node.js’s native test framework with before/after hooks and assertion utilities. Tests follow a consistent pattern of setting up hook configurations, executing markdown parsing, and verifying output HTML structure. Async/await patterns are extensively used for testing asynchronous hook operations.

Technical Details

Testing infrastructure includes:
  • Node.js test runner
  • ESM module system
  • Custom utility functions for timeout simulation
  • Marked parser instance management
  • Token creation helpers

Best Practices Demonstrated

The test suite exemplifies several testing best practices:
  • Isolated test cases with proper setup/teardown
  • Comprehensive async operation validation
  • Edge case handling for different hook combinations
  • Clear test descriptions and assertions
  • Modular test organization

markedjs/marked

test/unit/Hooks.test.js

            
import { Marked } from '../../lib/marked.esm.js';
import { timeout } from './utils.js';
import { describe, it, beforeEach } from 'node:test';
import assert from 'node:assert';

function createHeadingToken(text) {
  return {
    type: 'heading',
    raw: `# ${text}`,
    depth: 1,
    text,
    tokens: [
      { type: 'text', raw: text, text },
    ],
  };
}

describe('Hooks', () => {
  let marked;
  beforeEach(() => {
    marked = new Marked();
  });

  it('should preprocess markdown', () => {
    marked.use({
      hooks: {
        preprocess(markdown) {
          return `# preprocess\n\n${markdown}`;
        },
      },
    });
    const html = marked.parse('*text*');
    assert.strictEqual(html.trim(), '<h1>preprocess</h1>\n<p><em>text</em></p>');
  });

  it('should preprocess async', async() => {
    marked.use({
      async: true,
      hooks: {
        async preprocess(markdown) {
          await timeout();
          return `# preprocess async\n\n${markdown}`;
        },
      },
    });
    const promise = marked.parse('*text*');
    assert.ok(promise instanceof Promise);
    const html = await promise;
    assert.strictEqual(html.trim(), '<h1>preprocess async</h1>\n<p><em>text</em></p>');
  });

  it('should preprocess options', () => {
    marked.use({
      hooks: {
        preprocess(markdown) {
          this.options.breaks = true;
          return markdown;
        },
      },
    });
    const html = marked.parse('line1\nline2');
    assert.strictEqual(html.trim(), '<p>line1<br>line2</p>');
  });

  it('should preprocess options async', async() => {
    marked.use({
      async: true,
      hooks: {
        async preprocess(markdown) {
          await timeout();
          this.options.breaks = true;
          return markdown;
        },
      },
    });
    const html = await marked.parse('line1\nline2');
    assert.strictEqual(html.trim(), '<p>line1<br>line2</p>');
  });

  it('should postprocess html', () => {
    marked.use({
      hooks: {
        postprocess(html) {
          return html + '<h1>postprocess</h1>';
        },
      },
    });
    const html = marked.parse('*text*');
    assert.strictEqual(html.trim(), '<p><em>text</em></p>\n<h1>postprocess</h1>');
  });

  it('should postprocess async', async() => {
    marked.use({
      async: true,
      hooks: {
        async postprocess(html) {
          await timeout();
          return html + '<h1>postprocess async</h1>\n';
        },
      },
    });
    const promise = marked.parse('*text*');
    assert.ok(promise instanceof Promise);
    const html = await promise;
    assert.strictEqual(html.trim(), '<p><em>text</em></p>\n<h1>postprocess async</h1>');
  });

  it('should process tokens before walkTokens', () => {
    marked.use({
      hooks: {
        processAllTokens(tokens) {
          tokens.push(createHeadingToken('processAllTokens'));
          return tokens;
        },
      },
      walkTokens(token) {
        if (token.type === 'heading') {
          token.tokens[0].text += ' walked';
        }
        return token;
      },
    });
    const html = marked.parse('*text*');
    assert.strictEqual(html.trim(), '<p><em>text</em></p>\n<h1>processAllTokens walked</h1>');
  });

  it('should process tokens async before walkTokens', async() => {
    marked.use({
      async: true,
      hooks: {
        async processAllTokens(tokens) {
          await timeout();
          tokens.push(createHeadingToken('processAllTokens async'));
          return tokens;
        },
      },
      walkTokens(token) {
        if (token.type === 'heading') {
          token.tokens[0].text += ' walked';
        }
        return token;
      },
    });
    const promise = marked.parse('*text*');
    assert.ok(promise instanceof Promise);
    const html = await promise;
    assert.strictEqual(html.trim(), '<p><em>text</em></p>\n<h1>processAllTokens async walked</h1>');
  });

  it('should process all hooks in reverse', async() => {
    marked.use({
      hooks: {
        preprocess(markdown) {
          return `# preprocess1\n\n${markdown}`;
        },
        postprocess(html) {
          return html + '<h1>postprocess1</h1>\n';
        },
        processAllTokens(tokens) {
          tokens.push(createHeadingToken('processAllTokens1'));
          return tokens;
        },
      },
    });
    marked.use({
      async: true,
      hooks: {
        preprocess(markdown) {
          return `# preprocess2\n\n${markdown}`;
        },
        async postprocess(html) {
          await timeout();
          return html + '<h1>postprocess2 async</h1>\n';
        },
        processAllTokens(tokens) {
          tokens.push(createHeadingToken('processAllTokens2'));
          return tokens;
        },
      },
    });
    const promise = marked.parse('*text*');
    assert.ok(promise instanceof Promise);
    const html = await promise;
    assert.strictEqual(html.trim(), `\
<h1>preprocess1</h1>
<h1>preprocess2</h1>
<p><em>text</em></p>
<h1>processAllTokens2</h1>
<h1>processAllTokens1</h1>
<h1>postprocess2 async</h1>
<h1>postprocess1</h1>`);
  });

  it('should provide lexer', () => {
    marked.use({
      hooks: {
        provideLexer() {
          return (src) => [createHeadingToken(src)];
        },
      },
    });
    const html = marked.parse('text');
    assert.strictEqual(html.trim(), '<h1>text</h1>');
  });

  it('should provide lexer async', async() => {
    marked.use({
      async: true,
      hooks: {
        provideLexer() {
          return async(src) => {
            await timeout();
            return [createHeadingToken(src)];
          };
        },
      },
    });
    const html = await marked.parse('text');
    assert.strictEqual(html.trim(), '<h1>text</h1>');
  });

  it('should provide parser return object', () => {
    marked.use({
      hooks: {
        provideParser() {
          return (tokens) => ({ text: 'test parser' });
        },
      },
    });
    const html = marked.parse('text');
    assert.strictEqual(html.text, 'test parser');
  });

  it('should provide parser', () => {
    marked.use({
      hooks: {
        provideParser() {
          return (tokens) => 'test parser';
        },
      },
    });
    const html = marked.parse('text');
    assert.strictEqual(html.trim(), 'test parser');
  });

  it('should provide parser async', async() => {
    marked.use({
      async: true,
      hooks: {
        provideParser() {
          return async(tokens) => {
            await timeout();
            return 'test parser';
          };
        },
      },
    });
    const html = await marked.parse('text');
    assert.strictEqual(html.trim(), 'test parser');
  });
});