From 09e91939d880db789494e059b3ae5f8b3fbca5dc Mon Sep 17 00:00:00 2001 From: Remco Haszing Date: Sat, 1 Mar 2025 12:39:58 +0100 Subject: [PATCH 1/2] 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. --- lib/index.js | 3 -- package.json | 2 + test.jsx | 136 +++++++++++++++++++++++++++++++++++++++++---------- 3 files changed, 112 insertions(+), 29 deletions(-) diff --git a/lib/index.js b/lib/index.js index 7696047..81aba70 100644 --- a/lib/index.js +++ b/lib/index.js @@ -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) { @@ -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) } diff --git a/package.json b/package.json index 4229029..4763e4a 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ }, "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", @@ -69,6 +70,7 @@ "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", diff --git a/test.jsx b/test.jsx index b59702f..574621b 100644 --- a/test.jsx +++ b/test.jsx @@ -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 'global-jsdom/register' import assert from 'node:assert/strict' import test from 'node:test' +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' @@ -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(), '') - }) + 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() - .pipe( - concatStream({encoding: 'u8'}, function (data) { - assert.equal(decoder.decode(data), '') - resolve() - }) - ) - .on('error', reject) + const {container} = render( + + ) + + assert.equal(container.innerHTML, '') + plugin.resolve() + await waitFor(() => { + assert.notEqual(container.innerHTML, '') }) + assert.equal(container.innerHTML, '

a

') }) await t.test( 'should support async plugins w/ `MarkdownHooks` (`rehype-starry-night`)', async function () { - return new Promise(function (resolve) { - renderToPipeableStream( - - ).pipe( - concatStream({encoding: 'u8'}, function (data) { - assert.equal(decoder.decode(data), '') - resolve() - }) - ) + const plugin = deferPlugin() + + const {container} = render( + + ) + + assert.equal(container.innerHTML, '') + plugin.resolve() + await waitFor(() => { + assert.notEqual(container.innerHTML, '') }) + assert.equal( + container.innerHTML, + '
console.log(3.14)\n
' + ) } ) + + await t.test('should support `MarkdownHooks` that error', async function () { + const plugin = deferPlugin() + + const {container} = render( + + + + ) + + 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} */ + 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 + } +} From 6ee0ff53891b173207092550fccae31595a1d493 Mon Sep 17 00:00:00 2001 From: Remco Haszing Date: Sat, 1 Mar 2025 13:01:42 +0100 Subject: [PATCH 2/2] Fix linting error --- test.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test.jsx b/test.jsx index 574621b..8bbf4f7 100644 --- a/test.jsx +++ b/test.jsx @@ -6,9 +6,9 @@ * @import {Plugin} from 'unified' */ -import 'global-jsdom/register' 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'