Понимание структуры плагинов
Плагины для Remark и Rehype строятся вокруг концепции обработки абстрактного синтаксического дерева (AST). Remark работает с Markdown AST (MDAST), а Rehype — с HTML AST (HAST). Каждый плагин — это функция, которая принимает дерево и модифицирует его, добавляет узлы, удаляет или преобразует существующие.
Простейший шаблон плагина Remark выглядит так:
function myRemarkPlugin(options = {}) {
return (tree, file) => {
// tree — MDAST
// file — объект VFile, содержит метаданные и содержимое
};
}
Для Rehype структура аналогична, но дерево уже HAST:
function myRehypePlugin(options = {}) {
return (tree, file) => {
// tree — HAST
// file — объект VFile
};
}
Выбор подхода к юнит-тестированию
Юнит-тесты плагинов сосредотачиваются на проверке корректности преобразований AST. Основные стратегии:
Сравнение исходного и ожидаемого AST После применения плагина создается дерево, которое сравнивается с заранее подготовленным эталонным AST. Этот метод хорошо подходит для точных трансформаций, где структура узлов критична.
Сравнение результирующего Markdown/HTML После применения плагина AST преобразуется обратно в Markdown или HTML и сравнивается с ожидаемым текстом. Этот подход удобен, когда важен конечный результат, а не структура дерева.
Снапшоты Фреймворки вроде Jest позволяют сохранять снимки AST или HTML и автоматически проверять изменения. Удобно для крупных плагинов с множеством вариантов преобразований.
Инструменты для тестирования
Пример теста плагина Remark
Плагин добавляет в конец документа заголовок:
function addFooterHeader() {
return (tree) => {
const headingNode = {
type: 'heading',
depth: 2,
children: [{ type: 'text', value: 'Footer' }]
};
tree.children.push(headingNode);
};
}
// Тестирование с использованием Jest
import {unified} from 'unified';
import remarkParse from 'remark-parse';
import remarkStringify from 'remark-stringify';
test('добавление заголовка в конец документа', async () => {
const processor = unified()
.use(remarkParse)
.use(addFooterHeader)
.use(remarkStringify);
const input = '## Title\n\nSome text.';
const output = String(await processor.process(input));
expect(output).toContain('## Footer');
});
Ключевой момент: тест проверяет только эффект плагина на дерево и финальный Markdown, не завися от внутренних деталей обработки.
Тестирование Rehype плагинов
Для Rehype аналогичный подход, но с HTML:
function addClassToParagraphs() {
return (tree) => {
visit(tree, 'element', (node) => {
if (node.tagName === 'p') {
node.properties = node.properties || {};
node.properties.className = ['highlight'];
}
});
};
}
// Тест с использованием unified и rehype
import {unified} from 'unified';
import rehypeParse from 'rehype-parse';
import rehypeStringify from 'rehype-stringify';
import {visit} from 'unist-util-visit';
test('добавление класса ко всем параграфам', async () => {
const processor = unified()
.use(rehypeParse, {fragment: true})
.use(addClassToParagraphs)
.use(rehypeStringify);
const input = '<p>Hello</p><p>World</p>';
const output = String(await processor.process(input));
expect(output).toContain('<p class="highlight">Hello</p>');
expect(output).toContain('<p class="highlight">World</p>');
});
Работа с асинхронными плагинами
Некоторые плагины выполняют асинхронные операции (загрузка внешних
данных, API-запросы). Для них важно использовать
async/await и корректно тестировать обработку:
function asyncPlugin() {
return async (tree) => {
await new Promise(resolve => setTimeout(resolve, 10));
tree.children.push({ type: 'text', value: 'Async content' });
};
}
test('асинхронное добавление контента', async () => {
const processor = unified().use(asyncPlugin);
const tree = { type: 'root', children: [] };
await processor.run(tree);
expect(tree.children[0].value).toBe('Async content');
});
Советы по надежному тестированию
expect(tree).toEqual(expectedTree), чтобы учесть все
свойства узлов.Отслеживание ошибок и сообщений
Плагины могут добавлять сообщения об ошибках через объект
VFile. Тестирование таких сообщений позволяет убедиться,
что плагин корректно реагирует на неправильный вход:
function errorPlugin() {
return (tree, file) => {
if (!tree.children.length) {
file.message('Документ пустой');
}
};
}
test('сообщение об ошибке при пустом дереве', () => {
const file = { messages: [] };
errorPlugin()({ type: 'root', children: [] }, file);
expect(file.messages[0].reason).toBe('Документ пустой');
});
Заключение по подходам
Эффективное юнит-тестирование плагинов Remark и Rehype строится на тщательной проверке работы с AST, контроле преобразований и обработке ошибок. Использование синхронного и асинхронного API unified позволяет создавать быстрые, надежные тесты, которые покрывают все ключевые сценарии работы плагина.