Skip to content

Commit

Permalink
Add lifecycle tests for MarkdownHooks
Browse files Browse the repository at this point in the history
Closes GH-894.

Reviewed-by: Christian Murphy <[email protected]>
Reviewed-by: Titus Wormer <[email protected]>
  • Loading branch information
remcohaszing authored Mar 3, 2025
1 parent 2792c32 commit ad7f37f
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 29 deletions.
3 changes: 0 additions & 3 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,6 @@ export function MarkdownHooks(options) {
const [tree, setTree] = useState(/** @type {Root | undefined} */ (undefined))

useEffect(
/* c8 ignore next 7 -- hooks are client-only. */
function () {
const file = createFile(options)
processor.run(processor.parse(file), file, function (error, tree) {
Expand All @@ -222,10 +221,8 @@ export function MarkdownHooks(options) {
]
)

/* c8 ignore next -- hooks are client-only. */
if (error) throw error

/* c8 ignore next -- hooks are client-only. */
return tree ? post(tree, options) : createElement(Fragment)
}

Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,15 @@
},
"description": "React component to render markdown",
"devDependencies": {
"@testing-library/react": "^16.0.0",
"@types/node": "^22.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"c8": "^10.0.0",
"concat-stream": "^2.0.0",
"esbuild": "^0.25.0",
"eslint-plugin-react": "^7.0.0",
"global-jsdom": "^26.0.0",
"prettier": "^3.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
Expand Down
136 changes: 110 additions & 26 deletions test.jsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
/* @jsxRuntime automatic @jsxImportSource react */
/**
* @import {Root} from 'hast'
* @import {ComponentProps} from 'react'
* @import {ComponentProps, ReactNode} from 'react'
* @import {ExtraProps} from 'react-markdown'
* @import {Plugin} from 'unified'
*/

import assert from 'node:assert/strict'
import test from 'node:test'
import 'global-jsdom/register'
import {render, waitFor} from '@testing-library/react'
import concatStream from 'concat-stream'
import {Component} from 'react'
import {renderToPipeableStream, renderToStaticMarkup} from 'react-dom/server'
import Markdown, {MarkdownAsync, MarkdownHooks} from 'react-markdown'
import rehypeRaw from 'rehype-raw'
Expand Down Expand Up @@ -1106,39 +1110,119 @@ test('MarkdownAsync', async function (t) {

// Note: hooks are not supported on the “server”.
test('MarkdownHooks', async function (t) {
await t.test('should support `MarkdownHooks` (1)', async function () {
assert.equal(renderToStaticMarkup(<MarkdownHooks children={'a'} />), '')
})
await t.test('should support `MarkdownHooks`', async function () {
const plugin = deferPlugin()

await t.test('should support `MarkdownHooks` (2)', async function () {
return new Promise(function (resolve, reject) {
renderToPipeableStream(<MarkdownHooks children={'a'} />)
.pipe(
concatStream({encoding: 'u8'}, function (data) {
assert.equal(decoder.decode(data), '')
resolve()
})
)
.on('error', reject)
const {container} = render(
<MarkdownHooks children={'a'} rehypePlugins={[plugin.plugin]} />
)

assert.equal(container.innerHTML, '')
plugin.resolve()
await waitFor(() => {
assert.notEqual(container.innerHTML, '')
})
assert.equal(container.innerHTML, '<p>a</p>')
})

await t.test(
'should support async plugins w/ `MarkdownHooks` (`rehype-starry-night`)',
async function () {
return new Promise(function (resolve) {
renderToPipeableStream(
<MarkdownHooks
children={'```js\nconsole.log(3.14)'}
rehypePlugins={[rehypeStarryNight]}
/>
).pipe(
concatStream({encoding: 'u8'}, function (data) {
assert.equal(decoder.decode(data), '')
resolve()
})
)
const plugin = deferPlugin()

const {container} = render(
<MarkdownHooks
children={'```js\nconsole.log(3.14)'}
rehypePlugins={[plugin.plugin, rehypeStarryNight]}
/>
)

assert.equal(container.innerHTML, '')
plugin.resolve()
await waitFor(() => {
assert.notEqual(container.innerHTML, '')
})
assert.equal(
container.innerHTML,
'<pre><code class="language-js"><span class="pl-en">console</span>.<span class="pl-c1">log</span>(<span class="pl-c1">3.14</span>)\n</code></pre>'
)
}
)

await t.test('should support `MarkdownHooks` that error', async function () {
const plugin = deferPlugin()

const {container} = render(
<ErrorBoundary>
<MarkdownHooks children={'a'} rehypePlugins={[plugin.plugin]} />
</ErrorBoundary>
)

assert.equal(container.innerHTML, '')
plugin.reject(new Error('rejected'))
await waitFor(() => {
assert.notEqual(container.innerHTML, '')
})
assert.equal(container.innerHTML, 'Error: rejected')
})
})

/**
* @typedef DeferredPlugin
* @property {Plugin<[]>} plugin
* A unified plugin
* @property {() => void} resolve
* Resolve the plugin.
* @property {(error: Error) => void} reject
* Reject the plugin.
*/

/**
* Create an async unified plugin which waits until a promise is resolved.
*
* @returns {DeferredPlugin}
* The plugin and resolver.
*/
function deferPlugin() {
/** @type {() => void} */
let res
/** @type {(error: Error) => void} */
let rej
/** @type {Promise<void>} */
const promise = new Promise((resolve, reject) => {
res = resolve
rej = reject
})

return {
resolve() {
res()
},
reject(error) {
rej(error)
},
plugin() {
return () => promise
}
}
}

class ErrorBoundary extends Component {
state = {
error: null
}

/**
* @param {Error} error
*/
componentDidCatch(error) {
this.setState({error})
}

render() {
const {children} = /** @type {{children: ReactNode}} */ (this.props)
const {error} = this.state

return error ? String(error) : children
}
}

0 comments on commit ad7f37f

Please sign in to comment.