|
1 | 1 | /* @jsxRuntime automatic @jsxImportSource react */
|
2 | 2 | /**
|
3 | 3 | * @import {Root} from 'hast'
|
4 |
| - * @import {ComponentProps} from 'react' |
| 4 | + * @import {ComponentProps, ReactNode} from 'react' |
5 | 5 | * @import {ExtraProps} from 'react-markdown'
|
| 6 | + * @import {Plugin} from 'unified' |
6 | 7 | */
|
7 | 8 |
|
8 | 9 | import assert from 'node:assert/strict'
|
9 | 10 | import test from 'node:test'
|
| 11 | +import 'global-jsdom/register' |
| 12 | +import {render, waitFor} from '@testing-library/react' |
10 | 13 | import concatStream from 'concat-stream'
|
| 14 | +import {Component} from 'react' |
11 | 15 | import {renderToPipeableStream, renderToStaticMarkup} from 'react-dom/server'
|
12 | 16 | import Markdown, {MarkdownAsync, MarkdownHooks} from 'react-markdown'
|
13 | 17 | import rehypeRaw from 'rehype-raw'
|
@@ -1106,39 +1110,119 @@ test('MarkdownAsync', async function (t) {
|
1106 | 1110 |
|
1107 | 1111 | // Note: hooks are not supported on the “server”.
|
1108 | 1112 | 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() |
1112 | 1115 |
|
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, '') |
1123 | 1124 | })
|
| 1125 | + assert.equal(container.innerHTML, '<p>a</p>') |
1124 | 1126 | })
|
1125 | 1127 |
|
1126 | 1128 | await t.test(
|
1127 | 1129 | 'should support async plugins w/ `MarkdownHooks` (`rehype-starry-night`)',
|
1128 | 1130 | 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, '') |
1141 | 1144 | })
|
| 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 | + ) |
1142 | 1149 | }
|
1143 | 1150 | )
|
| 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 | + }) |
1144 | 1168 | })
|
| 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