Skip to content

Commit ad7f37f

Browse files
authored
Add lifecycle tests for MarkdownHooks
Closes GH-894. Reviewed-by: Christian Murphy <[email protected]> Reviewed-by: Titus Wormer <[email protected]>
1 parent 2792c32 commit ad7f37f

File tree

3 files changed

+112
-29
lines changed

3 files changed

+112
-29
lines changed

lib/index.js

-3
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,6 @@ export function MarkdownHooks(options) {
206206
const [tree, setTree] = useState(/** @type {Root | undefined} */ (undefined))
207207

208208
useEffect(
209-
/* c8 ignore next 7 -- hooks are client-only. */
210209
function () {
211210
const file = createFile(options)
212211
processor.run(processor.parse(file), file, function (error, tree) {
@@ -222,10 +221,8 @@ export function MarkdownHooks(options) {
222221
]
223222
)
224223

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

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

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,15 @@
6262
},
6363
"description": "React component to render markdown",
6464
"devDependencies": {
65+
"@testing-library/react": "^16.0.0",
6566
"@types/node": "^22.0.0",
6667
"@types/react": "^19.0.0",
6768
"@types/react-dom": "^19.0.0",
6869
"c8": "^10.0.0",
6970
"concat-stream": "^2.0.0",
7071
"esbuild": "^0.25.0",
7172
"eslint-plugin-react": "^7.0.0",
73+
"global-jsdom": "^26.0.0",
7274
"prettier": "^3.0.0",
7375
"react": "^19.0.0",
7476
"react-dom": "^19.0.0",

test.jsx

+110-26
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
/* @jsxRuntime automatic @jsxImportSource react */
22
/**
33
* @import {Root} from 'hast'
4-
* @import {ComponentProps} from 'react'
4+
* @import {ComponentProps, ReactNode} from 'react'
55
* @import {ExtraProps} from 'react-markdown'
6+
* @import {Plugin} from 'unified'
67
*/
78

89
import assert from 'node:assert/strict'
910
import test from 'node:test'
11+
import 'global-jsdom/register'
12+
import {render, waitFor} from '@testing-library/react'
1013
import concatStream from 'concat-stream'
14+
import {Component} from 'react'
1115
import {renderToPipeableStream, renderToStaticMarkup} from 'react-dom/server'
1216
import Markdown, {MarkdownAsync, MarkdownHooks} from 'react-markdown'
1317
import rehypeRaw from 'rehype-raw'
@@ -1106,39 +1110,119 @@ test('MarkdownAsync', async function (t) {
11061110

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

1113-
await t.test('should support `MarkdownHooks` (2)', async function () {
1114-
return new Promise(function (resolve, reject) {
1115-
renderToPipeableStream(<MarkdownHooks children={'a'} />)
1116-
.pipe(
1117-
concatStream({encoding: 'u8'}, function (data) {
1118-
assert.equal(decoder.decode(data), '')
1119-
resolve()
1120-
})
1121-
)
1122-
.on('error', reject)
1116+
const {container} = render(
1117+
<MarkdownHooks children={'a'} rehypePlugins={[plugin.plugin]} />
1118+
)
1119+
1120+
assert.equal(container.innerHTML, '')
1121+
plugin.resolve()
1122+
await waitFor(() => {
1123+
assert.notEqual(container.innerHTML, '')
11231124
})
1125+
assert.equal(container.innerHTML, '<p>a</p>')
11241126
})
11251127

11261128
await t.test(
11271129
'should support async plugins w/ `MarkdownHooks` (`rehype-starry-night`)',
11281130
async function () {
1129-
return new Promise(function (resolve) {
1130-
renderToPipeableStream(
1131-
<MarkdownHooks
1132-
children={'```js\nconsole.log(3.14)'}
1133-
rehypePlugins={[rehypeStarryNight]}
1134-
/>
1135-
).pipe(
1136-
concatStream({encoding: 'u8'}, function (data) {
1137-
assert.equal(decoder.decode(data), '')
1138-
resolve()
1139-
})
1140-
)
1131+
const plugin = deferPlugin()
1132+
1133+
const {container} = render(
1134+
<MarkdownHooks
1135+
children={'```js\nconsole.log(3.14)'}
1136+
rehypePlugins={[plugin.plugin, rehypeStarryNight]}
1137+
/>
1138+
)
1139+
1140+
assert.equal(container.innerHTML, '')
1141+
plugin.resolve()
1142+
await waitFor(() => {
1143+
assert.notEqual(container.innerHTML, '')
11411144
})
1145+
assert.equal(
1146+
container.innerHTML,
1147+
'<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>'
1148+
)
11421149
}
11431150
)
1151+
1152+
await t.test('should support `MarkdownHooks` that error', async function () {
1153+
const plugin = deferPlugin()
1154+
1155+
const {container} = render(
1156+
<ErrorBoundary>
1157+
<MarkdownHooks children={'a'} rehypePlugins={[plugin.plugin]} />
1158+
</ErrorBoundary>
1159+
)
1160+
1161+
assert.equal(container.innerHTML, '')
1162+
plugin.reject(new Error('rejected'))
1163+
await waitFor(() => {
1164+
assert.notEqual(container.innerHTML, '')
1165+
})
1166+
assert.equal(container.innerHTML, 'Error: rejected')
1167+
})
11441168
})
1169+
1170+
/**
1171+
* @typedef DeferredPlugin
1172+
* @property {Plugin<[]>} plugin
1173+
* A unified plugin
1174+
* @property {() => void} resolve
1175+
* Resolve the plugin.
1176+
* @property {(error: Error) => void} reject
1177+
* Reject the plugin.
1178+
*/
1179+
1180+
/**
1181+
* Create an async unified plugin which waits until a promise is resolved.
1182+
*
1183+
* @returns {DeferredPlugin}
1184+
* The plugin and resolver.
1185+
*/
1186+
function deferPlugin() {
1187+
/** @type {() => void} */
1188+
let res
1189+
/** @type {(error: Error) => void} */
1190+
let rej
1191+
/** @type {Promise<void>} */
1192+
const promise = new Promise((resolve, reject) => {
1193+
res = resolve
1194+
rej = reject
1195+
})
1196+
1197+
return {
1198+
resolve() {
1199+
res()
1200+
},
1201+
reject(error) {
1202+
rej(error)
1203+
},
1204+
plugin() {
1205+
return () => promise
1206+
}
1207+
}
1208+
}
1209+
1210+
class ErrorBoundary extends Component {
1211+
state = {
1212+
error: null
1213+
}
1214+
1215+
/**
1216+
* @param {Error} error
1217+
*/
1218+
componentDidCatch(error) {
1219+
this.setState({error})
1220+
}
1221+
1222+
render() {
1223+
const {children} = /** @type {{children: ReactNode}} */ (this.props)
1224+
const {error} = this.state
1225+
1226+
return error ? String(error) : children
1227+
}
1228+
}

0 commit comments

Comments
 (0)