| ... | ... | @@ -1,3 +1,5 @@ |
| 1 | // @vitest-environment jsdom |
| 2 | |
| 1 | 3 | import { EnvironmentId } from "@t3tools/contracts"; |
| 2 | 4 | import { act, type ComponentProps, type ReactNode } from "react"; |
| 3 | 5 | import { renderToStaticMarkup } from "react-dom/server"; |
| ... | ... | @@ -73,6 +75,82 @@ function codeButton(renderer: ReactTestRenderer, label: string) { |
| 73 | 75 | return button.props as ComponentProps<typeof Button>; |
| 74 | 76 | } |
| 75 | 77 | |
| 78 | describe("ChatMarkdown bare anchor placeholders", () => { |
| 79 | it.each(["<A>", "<a>", "<a >", "<a/>", "<A/>", "<a />"])( |
| 80 | "preserves unmatched %s without linking later blocks", |
| 81 | (token) => { |
| 82 | const text = `- **"From ${token}"** appears in the header.\n\n- **Tests:** cover inheritance.\n\nThe deferred move continues on B.\n\nSee <a href="https://example.com">the link</a>.`; |
| 83 | const document = new DOMParser().parseFromString( |
| 84 | renderToStaticMarkup(<ChatMarkdown cwd="/tmp/project" text={text} />), |
| 85 | "text/html", |
| 86 | ); |
| 87 | |
| 88 | expect(document.querySelector("strong")?.textContent).toBe(`"From ${token}"`); |
| 89 | expect([...document.querySelectorAll("a")].map((link) => link.textContent)).toEqual([ |
| 90 | "the link", |
| 91 | ]); |
| 92 | expect(document.querySelectorAll("li")).toHaveLength(2); |
| 93 | expect( |
| 94 | [...document.querySelectorAll("p")].map((paragraph) => paragraph.textContent), |
| 95 | ).toContain("The deferred move continues on B."); |
| 96 | }, |
| 97 | ); |
| 98 | |
| 99 | it.each(["</a> ", "<div>more</div>\n</a>"])( |
| 100 | "preserves a paired anchor closing in the raw block %s", |
| 101 | (closing) => { |
| 102 | const document = new DOMParser().parseFromString( |
| 103 | renderToStaticMarkup( |
| 104 | <ChatMarkdown cwd="/tmp/project" text={`See <a>label\n\n${closing}\n\nfinish`} />, |
| 105 | ), |
| 106 | "text/html", |
| 107 | ); |
| 108 | expect(document.querySelector("p")?.textContent).toBe("See label"); |
| 109 | }, |
| 110 | ); |
| 111 | |
| 112 | it("preserves a paired anchor after comment-looking raw text", () => { |
| 113 | const document = new DOMParser().parseFromString( |
| 114 | renderToStaticMarkup( |
| 115 | <ChatMarkdown cwd="/tmp/project" text="See <a>label<script><!-- </script> --></a>" />, |
| 116 | ), |
| 117 | "text/html", |
| 118 | ); |
| 119 | expect(document.querySelector("p")?.textContent).toBe("See label -->"); |
| 120 | }); |
| 121 | |
| 122 | it.each(["<!-- </a> -->", '<div title="</a>">more</div>', '<script>"</a>"</script>'])( |
| 123 | "ignores apparent closing anchors inside %s", |
| 124 | (html) => { |
| 125 | const document = new DOMParser().parseFromString( |
| 126 | renderToStaticMarkup( |
| 127 | <ChatMarkdown cwd="/tmp/project" text={`Before <A>.\n\n${html}\n\nAfter.`} />, |
| 128 | ), |
| 129 | "text/html", |
| 130 | ); |
| 131 | expect(document.querySelector("p")?.textContent).toBe("Before <A>."); |
| 132 | expect(document.querySelectorAll("a")).toHaveLength(0); |
| 133 | }, |
| 134 | ); |
| 135 | |
| 136 | it("preserves paired HTML anchors, details, markdown links, and inline code", () => { |
| 137 | const text = |
| 138 | 'Bare <a>label</a>, <a id="section"></a>, `<A>`, and [docs](https://example.com).\n\n<details><summary>More</summary>Details</details>'; |
| 139 | const document = new DOMParser().parseFromString( |
| 140 | renderToStaticMarkup(<ChatMarkdown cwd="/tmp/project" text={text} />), |
| 141 | "text/html", |
| 142 | ); |
| 143 | |
| 144 | expect([...document.querySelectorAll("a")].map((link) => link.textContent)).toEqual([ |
| 145 | "label", |
| 146 | "", |
| 147 | "docs", |
| 148 | ]); |
| 149 | expect(document.querySelector("code")?.textContent).toBe("<A>"); |
| 150 | expect(document.querySelector("[data-markdown-details]")?.textContent).toContain("More"); |
| 151 | }); |
| 152 | }); |
| 153 | |
| 76 | 154 | describe("ChatMarkdown context references", () => { |
| 77 | 155 | it("renders text and image references through the chip renderer, with readable fallback", async () => { |
| 78 | 156 | vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); |