Skip to content

Commit 09e9193

Browse files
committed
Add lifecycle tests for MarkdownHooks
This uses React Testing Library to test the hooks implementation. This gives us control over the React lifecycle, so we can test any state the component might be in.
1 parent 21b47b9 commit 09e9193

File tree

3 files changed

+112
-29
lines changed

3 files changed

+112
-29
lines changed

lib/index.js

Lines changed: 0 additions & 3 deletions
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 110 additions & 26 deletions
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

9+
import 'global-jsdom/register'
810
import assert from 'node:assert/strict'
911
import test from 'node:test'
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)