Skip to content

Testing Plugins

Generative DOM uses Vitest with a jsdom environment for testing. This guide explains how to test plugins effectively.

Test Setup

Create a test file for your plugin:

ts
import { describe, it, expect, beforeEach } from 'vitest';
import { GenerativeDom } from '@generative-dom/core';
import { markdownBase } from '@generative-dom/plugins';
import { myPlugin } from './my-plugin';

describe('myPlugin', () => {
  let container: HTMLElement;
  let md: GenerativeDom;

  beforeEach(() => {
    container = document.createElement('div');
    md = new GenerativeDom({
      container,
      plugins: [markdownBase(), myPlugin()],
    });
  });
});

Testing Block Matching

Test that your plugin correctly matches its syntax:

ts
it('matches callout syntax', () => {
  md.push('!!! note\nThis is a callout.\n!!!\n');
  md.flush();

  const callout = container.querySelector('.callout');
  expect(callout).not.toBeNull();
  expect(callout?.querySelector('strong')?.textContent).toBe('note');
  expect(callout?.querySelector('p')?.textContent).toBe('This is a callout.');
});

Testing Inline Matching

ts
it('matches highlight syntax', () => {
  md.push('This is ==highlighted== text.');
  md.flush();

  const mark = container.querySelector('mark');
  expect(mark).not.toBeNull();
  expect(mark?.textContent).toBe('highlighted');
});

Testing Streaming

Test that your plugin handles chunks correctly:

ts
it('handles stream splits', () => {
  // Split the input at every possible position
  const input = '!!! note\nCallout content.\n!!!\n';
  for (let i = 1; i < input.length; i++) {
    container.innerHTML = '';
    md.reset();
    md.push(input.slice(0, i));
    md.push(input.slice(i));
    md.flush();

    const callout = container.querySelector('.callout');
    expect(callout).not.toBeNull();
  }
});

it('handles single-character streaming', () => {
  const input = '!!! note\nCallout content.\n!!!\n';
  for (const char of input) {
    md.push(char);
  }
  md.flush();

  const callout = container.querySelector('.callout');
  expect(callout).not.toBeNull();
});

Testing Edge Cases

ts
it('ignores incomplete syntax', () => {
  md.push('!!! note\nNo closing fence');
  md.flush();

  // Should not render as callout -- syntax is incomplete
  const callout = container.querySelector('.callout');
  expect(callout).toBeNull();
});

it('handles empty content', () => {
  md.push('!!! note\n\n!!!\n');
  md.flush();

  const callout = container.querySelector('.callout');
  expect(callout).not.toBeNull();
});

Testing Events

If your plugin emits events, test them with a spy:

ts
it('emits events', () => {
  const handler = vi.fn();
  md.on('my-plugin:custom-event', handler);

  md.push('<my-syntax />');
  md.flush();

  expect(handler).toHaveBeenCalledOnce();
  expect(handler).toHaveBeenCalledWith({ key: 'value' });
});

Testing Security

Verify that your plugin does not introduce XSS vectors:

ts
it('does not allow script injection', () => {
  md.push('!!! <script>alert("xss")</script>\nContent\n!!!\n');
  md.flush();

  expect(container.querySelector('script')).toBeNull();
  // Verify the text appears as text, not as an element
  expect(container.textContent).toContain('<script>');
});

it('does not use innerHTML', () => {
  const spy = vi.spyOn(HTMLElement.prototype, 'innerHTML', 'set');
  md.push('!!! note\nContent\n!!!\n');
  md.flush();
  expect(spy).not.toHaveBeenCalled();
  spy.mockRestore();
});

Testing Cleanup

ts
it('cleans up on reset', () => {
  md.push('!!! note\nContent\n!!!\n');
  md.flush();
  expect(container.querySelector('.callout')).not.toBeNull();

  md.reset();
  expect(container.querySelector('.callout')).toBeNull();
});

it('cleans up on destroy', () => {
  md.push('!!! note\nContent\n!!!\n');
  md.flush();

  md.destroy();
  expect(container.innerHTML).toBe('');
});

Using StreamSimulator

The mocks package provides a StreamSimulator for testing with different chunk strategies:

ts
import { StreamSimulator } from '@generative-dom/mocks';

it('works with random chunk sizes', async () => {
  const sim = new StreamSimulator(md, {
    chunkStrategy: 'random',
    minChunkSize: 1,
    maxChunkSize: 5,
    delayMs: 0,
  });

  await sim.stream('!!! note\nCallout content.\n!!!\n');
  md.flush();

  expect(container.querySelector('.callout')).not.toBeNull();
});

Running Tests

bash
# Run all tests
pnpm test

# Run tests for a specific plugin
pnpm test -- --filter my-plugin

# Run in watch mode
pnpm test -- --watch