| ... | @@ -1,3 +1,5 @@ | ... | @@ -1,3 +1,5 @@ |
| | 1 | // @vitest-environment jsdom |
| | 2 | |
| 1 | import { EnvironmentId } from "@t3tools/contracts"; | 3 | import { EnvironmentId } from "@t3tools/contracts"; |
| 2 | import { act, type ComponentProps, type ReactNode } from "react"; | 4 | import { act, type ComponentProps, type ReactNode } from "react"; |
| 3 | import { renderToStaticMarkup } from "react-dom/server"; | 5 | import { renderToStaticMarkup } from "react-dom/server"; |
| ... | @@ -73,6 +75,82 @@ function codeButton(renderer: ReactTestRenderer, label: string) { | ... | @@ -73,6 +75,82 @@ function codeButton(renderer: ReactTestRenderer, label: string) { |
| 73 | return button.props as ComponentProps<typeof Button>; | 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 | describe("ChatMarkdown context references", () => { | 154 | describe("ChatMarkdown context references", () => { |
| 77 | it("renders text and image references through the chip renderer, with readable fallback", async () => { | 155 | it("renders text and image references through the chip renderer, with readable fallback", async () => { |
| 78 | vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); | 156 | vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); |