diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..4618aa8 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,74 @@ +name: Test + +on: + pull_request: + branches: [main] + paths: + - 'lib/**' + - 'index.ts' + - 'styles.css' + - 'package.json' + - '__tests__/**' + - 'tsconfig.json' + - '.github/workflows/test.yml' + push: + branches: [main] + paths: + - 'lib/**' + - 'index.ts' + - 'styles.css' + - 'package.json' + - '__tests__/**' + - 'tsconfig.json' + - '.github/workflows/test.yml' + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18, 20, 22] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'yarn' + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Build package + run: yarn build + + - name: Run all tests + run: yarn test:all + + - name: Check for TypeScript errors + run: yarn build:ts + + lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'yarn' + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Build package + run: yarn build + + - name: Check TypeScript types + run: yarn build:ts --noEmit diff --git a/.gitignore b/.gitignore index db4c6d9..4f9cbbe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ dist -node_modules \ No newline at end of file +node_modules +.DS_Store \ No newline at end of file diff --git a/README.md b/README.md index 08e0ee2..53e2d3b 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A powerful TypeScript remark plugin that transforms markdown blockquotes into beautifully styled note elements. Add professional-looking notes, tips, quotes, and more to your markdown documentation with minimal effort! -![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/rishichawda/remark-notes-plugin/release.yml) +![Test Status](https://img.shields.io/github/actions/workflow/status/rishichawda/remark-notes-plugin/test.yml?branch=main&label=tests) ![npm](https://img.shields.io/npm/v/remark-notes-plugin) ![License](https://img.shields.io/npm/l/remark-notes-plugin) ![Website](https://img.shields.io/website?url=https%3A%2F%2Frishichawda.github.io%2Fremark-notes-plugin) diff --git a/__tests__/README.md b/__tests__/README.md new file mode 100644 index 0000000..3c74e78 --- /dev/null +++ b/__tests__/README.md @@ -0,0 +1,80 @@ +# Tests + +## Test Structure + +### Test Files + +- **`index.ts`**: Fixture-based tests for plugin transformation + - Tests each note type (note, tip, important, quote, bonus) + - Compares actual output against known-good expected output + - Uses `rehype-parse` to parse HTML into HAST for structural comparison + - Strips position data for stable comparisons + +- **`styles.ts`**: Tests for automatic style injection + - Verifies styles are automatically injected + - Ensures styles are injected only once per document + - Validates CSS content is not empty + +- **`generate-fixtures.ts`**: Utility script to regenerate expected outputs + - Run when you make intentional changes to plugin output. MUST be done with caution. + - Updates all fixture outputs to match current implementation + +### Fixtures Directory (`__fixtures__/`) + +Each note type has its own directory containing: + +- `input.md`: Sample markdown input for that note type +- `output.html`: Expected HTML output (auto-generated) + +Structure: + +``` +__fixtures__/ +├── note/ +│ ├── input.md +│ └── output.html +├── tip/ +│ ├── input.md +│ └── output.html +├── important/ +│ ├── input.md +│ └── output.html +├── quote/ +│ ├── input.md +│ └── output.html +└── bonus/ + ├── input.md + └── output.html +``` + +## Running Tests + +```bash +# Run all tests +yarn test + +# Run fixture tests only +yarn test:fixtures + +# Run style injection tests only +yarn test:styles + +# Regenerate expected outputs after changes +yarn generate:fixtures +``` + +## Testing Approach + +1. **Fixture-based testing**: Instead of inline test data, we use separate files for inputs and expected outputs +2. **Structural comparison**: Uses `rehype-parse` to parse HTML into HAST (Hypertext Abstract Syntax Tree) for comparison +3. **Position-agnostic**: Strips position data from HAST to avoid tests that break on whitespace changes +4. **Separation of concerns**: Plugin transformation tests are separate from style injection tests + +## Adding New Test Cases + +To add a new test case: + +1. Create a new directory in `__fixtures__/` (e.g., `__fixtures__/new-type/`) +2. Add `input.md` with your test markdown +3. Run `yarn generate:fixtures` to auto-generate the expected `output.html` +4. Add the new type to the `FIXTURE_TYPES` array in `index.ts` diff --git a/__tests__/__fixtures__/bonus/input.md b/__tests__/__fixtures__/bonus/input.md new file mode 100644 index 0000000..6d6f859 --- /dev/null +++ b/__tests__/__fixtures__/bonus/input.md @@ -0,0 +1,3 @@ +> [!bonus] +> This is a bonus block. +> Extra info for advanced users. \ No newline at end of file diff --git a/__tests__/__fixtures__/bonus/output.html b/__tests__/__fixtures__/bonus/output.html new file mode 100644 index 0000000..8e7b49b --- /dev/null +++ b/__tests__/__fixtures__/bonus/output.html @@ -0,0 +1,2 @@ +
bonus

This is a bonus block. +Extra info for advanced users.

\ No newline at end of file diff --git a/__tests__/__fixtures__/important/input.md b/__tests__/__fixtures__/important/input.md new file mode 100644 index 0000000..0739edb --- /dev/null +++ b/__tests__/__fixtures__/important/input.md @@ -0,0 +1,3 @@ +> [!important] +> This is an important block. +> Pay attention to this! \ No newline at end of file diff --git a/__tests__/__fixtures__/important/output.html b/__tests__/__fixtures__/important/output.html new file mode 100644 index 0000000..0927126 --- /dev/null +++ b/__tests__/__fixtures__/important/output.html @@ -0,0 +1,2 @@ +
important

This is an important block. +Pay attention to this!

\ No newline at end of file diff --git a/__tests__/__fixtures__/note/input.md b/__tests__/__fixtures__/note/input.md new file mode 100644 index 0000000..955e6f7 --- /dev/null +++ b/__tests__/__fixtures__/note/input.md @@ -0,0 +1,3 @@ +> [!note] +> This is a note block. +> It supports **bold** and *italic* text. \ No newline at end of file diff --git a/__tests__/__fixtures__/note/output.html b/__tests__/__fixtures__/note/output.html new file mode 100644 index 0000000..debfa8a --- /dev/null +++ b/__tests__/__fixtures__/note/output.html @@ -0,0 +1,2 @@ +
note

This is a note block. +It supports bold and italic text.

\ No newline at end of file diff --git a/__tests__/__fixtures__/quote/input.md b/__tests__/__fixtures__/quote/input.md new file mode 100644 index 0000000..633b3e4 --- /dev/null +++ b/__tests__/__fixtures__/quote/input.md @@ -0,0 +1,3 @@ +> [!quote] +> This is a quote block. +> "To be or not to be." \ No newline at end of file diff --git a/__tests__/__fixtures__/quote/output.html b/__tests__/__fixtures__/quote/output.html new file mode 100644 index 0000000..632c54b --- /dev/null +++ b/__tests__/__fixtures__/quote/output.html @@ -0,0 +1,2 @@ +
quote

This is a quote block. +"To be or not to be."

\ No newline at end of file diff --git a/__tests__/__fixtures__/tip/input.md b/__tests__/__fixtures__/tip/input.md new file mode 100644 index 0000000..e7ea50a --- /dev/null +++ b/__tests__/__fixtures__/tip/input.md @@ -0,0 +1,3 @@ +> [!tip] +> This is a tip block. +> Use it for helpful advice. \ No newline at end of file diff --git a/__tests__/__fixtures__/tip/output.html b/__tests__/__fixtures__/tip/output.html new file mode 100644 index 0000000..f0ce5b6 --- /dev/null +++ b/__tests__/__fixtures__/tip/output.html @@ -0,0 +1,2 @@ +
tip

This is a tip block. +Use it for helpful advice.

\ No newline at end of file diff --git a/__tests__/custom-prefix.ts b/__tests__/custom-prefix.ts new file mode 100644 index 0000000..6aedc59 --- /dev/null +++ b/__tests__/custom-prefix.ts @@ -0,0 +1,134 @@ +/** + * Test custom class prefix option + */ + +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { unified } from 'unified' +import remarkParse from 'remark-parse' +import remarkRehype from 'remark-rehype' +import rehypeRaw from 'rehype-raw' +import rehypeStringify from 'rehype-stringify' +import remarkNotes from '../index.js' + +test('remark-notes: custom classPrefix should apply to all elements', async () => { + const markdown = '> [!tip]\n> This is a tip' + + const file = await unified() + .use(remarkParse) + .use(remarkNotes, { classPrefix: 'my' }) + .use(remarkRehype, { allowDangerousHtml: true }) + .use(rehypeRaw) + .use(rehypeStringify) + .process(markdown) + + const output = String(file) + + // Container should have both base and type modifier classes with prefix prepended + assert.ok(output.includes('class="my-remark-note my-remark-note-tip"'), + 'Container should have prefix prepended to standard remark-note classes') + + // Header, icon, title, and content should all use the prefix prepended to remark-note + assert.ok(output.includes('class="my-remark-note-header"'), + 'Header should use prefix-remark-note-header pattern') + assert.ok(output.includes('class="my-remark-note-icon"'), + 'Icon should use prefix-remark-note-icon pattern') + assert.ok(output.includes('class="my-remark-note-title"'), + 'Title should use prefix-remark-note-title pattern') + assert.ok(output.includes('class="my-remark-note-content"'), + 'Content should use prefix-remark-note-content pattern') + + // Should still contain 'remark-note' as part of the class names + assert.ok(output.includes('remark-note'), + 'Should contain remark-note as part of class names') +}) + +test('remark-notes: all note types work with custom prefix', async () => { + const noteTypes = ['note', 'tip', 'important', 'quote', 'bonus'] + const customPrefix = 'custom' + + for (const type of noteTypes) { + const markdown = `> [!${type}]\n> Test ${type}` + + const file = await unified() + .use(remarkParse) + .use(remarkNotes, { classPrefix: customPrefix }) + .use(remarkRehype, { allowDangerousHtml: true }) + .use(rehypeRaw) + .use(rehypeStringify) + .process(markdown) + + const output = String(file) + + // Check for prefix prepended to remark-note in container classes + assert.ok( + output.includes(`class="${customPrefix}-remark-note ${customPrefix}-remark-note-${type}"`), + `${type} should have prefix prepended: ${customPrefix}-remark-note ${customPrefix}-remark-note-${type}` + ) + + // Check for prefix prepended to remark-note in all sub-elements + assert.ok(output.includes(`class="${customPrefix}-remark-note-header"`), + `${type} header should use ${customPrefix}-remark-note-header`) + assert.ok(output.includes(`class="${customPrefix}-remark-note-icon"`), + `${type} icon should use ${customPrefix}-remark-note-icon`) + assert.ok(output.includes(`class="${customPrefix}-remark-note-title"`), + `${type} title should use ${customPrefix}-remark-note-title`) + assert.ok(output.includes(`class="${customPrefix}-remark-note-content"`), + `${type} content should use ${customPrefix}-remark-note-content`) + } +}) + +test('remark-notes: injectStyles option controls style injection', async () => { + const markdown = '> [!note]\n> Test note' + + // Test with injectStyles: true (default) + const fileWithStyles = await unified() + .use(remarkParse) + .use(remarkNotes, { injectStyles: true }) + .use(remarkRehype, { allowDangerousHtml: true }) + .use(rehypeStringify, { allowDangerousHtml: true }) + .process(markdown) + + const outputWithStyles = String(fileWithStyles) + assert.ok(outputWithStyles.includes('` - }); - hasInjectedStyles = true; + // Inject styles at the beginning of the document (only once) if enabled + // Using proper mdast node instead of HTML string for MDX compatibility + if (injectStyles && !hasInjectedStyles) { + const root = tree as Parent + if (root.children) { + // Create a proper mdast node that will be transformed to