diff --git a/tests/Memoizer.test.tsx b/tests/Memoizer.test.tsx index f34696681e9f4e1329d2a4240ffa9d63bf2bd721..dfff941c57acf292713740f2422f685af67569cb 100644 --- a/tests/Memoizer.test.tsx +++ b/tests/Memoizer.test.tsx @@ -17,6 +17,30 @@ import { } from "../src/Memoizer.ts"; const gfmProcessor = unified().use(remarkParse).use(remarkGfm); +const documentStateProcessor = unified() + .use(remarkParse) + .use(function documentStatePlugin() { + return (tree: Parent) => { + const paragraphs = tree.children.filter( + (node): node is Parent => + node.type === "paragraph" && "children" in node && Array.isArray(node.children), + ); + const suffix = paragraphs + .map((paragraph) => paragraphText(paragraph)) + .findLast((text) => text.startsWith("suffix: ")) + ?.slice("suffix: ".length); + if (!suffix) return tree; + + const target = paragraphs.find((paragraph) => !paragraphText(paragraph).startsWith("suffix: ")); + const firstChild = target?.children[0]; + if (firstChild?.type === "text") { + const textNode = firstChild as Literal; + const value = String(textNode.value); + textNode.value = `${value} (${suffix})`; + } + return tree; + }; + }); describe("Memoizer utilities", () => { it("finds the first differing index for edits in the middle, start, and end", () => { @@ -143,8 +167,8 @@ describe("Memoizer incremental rendering", () => { it("keeps earlier gfm footnotes rendered when a later footnote definition is removed", () => { const incremental = createMemoizer(gfmProcessor); - incremental.update("a[^b]\n\n[^b]: bee\n\nc"); - incremental.update("a[^b]\n\n[^b]: bee\n\nc[^d]\n\n[^d]: dee"); + void incremental.update("a[^b]\n\n[^b]: bee\n\nc"); + void incremental.update("a[^b]\n\n[^b]: bee\n\nc[^d]\n\n[^d]: dee"); const after = renderNodes(incremental.update("a[^b]\n\n[^b]: bee\n\nc[^d]")); const fresh = createMemoizer(gfmProcessor); @@ -222,6 +246,20 @@ describe("Memoizer incremental rendering", () => { expect(second[2]).toBe(first[2]); }); + + // TODO: this is a tough problem to cleanly solve without giving up too much memoization. + it.fails("re-renders earlier blocks when a later custom-plugin block changes document-wide state", () => { + const incremental = createMemoizer(documentStateProcessor); + void incremental.update("hello\n\nsuffix: one"); + + const after = renderNodes(incremental.update("hello\n\nsuffix: two")); + + const fresh = createMemoizer(documentStateProcessor); + const expected = renderNodes(fresh.update("hello\n\nsuffix: two")); + + expect(after).toBe(expected); + expect(expected).toContain("hello (two)"); + }); }); function createMemoizer(processor = defaultProcessor, predict = false) { @@ -233,3 +271,10 @@ function createMemoizer(processor = defaultProcessor, predict = false) { function renderNodes(nodes: readonly ReactNode[]) { return renderToStaticMarkup({nodes}); } + +function paragraphText(node: Parent) { + return node.children + .filter((child): child is Literal => child.type === "text") + .map((child) => String(child.value)) + .join(""); +}