11// @vitest -environment jsdom
22import { cleanup , createEvent , fireEvent , render , screen } from "@testing-library/react" ;
33import { afterEach , describe , expect , it , vi } from "vitest" ;
4+ import { expectOpenedFileTarget } from "../test/fileLinkAssertions" ;
45import { Markdown } from "./Markdown" ;
56
67describe ( "Markdown file-like href behavior" , ( ) => {
@@ -46,7 +47,7 @@ describe("Markdown file-like href behavior", () => {
4647 } ) ;
4748 fireEvent ( link as Element , clickEvent ) ;
4849 expect ( clickEvent . defaultPrevented ) . toBe ( true ) ;
49- expect ( onOpenFileLink ) . toHaveBeenCalledWith ( "./docs/setup.md" ) ;
50+ expectOpenedFileTarget ( onOpenFileLink , "./docs/setup.md" ) ;
5051 } ) ;
5152
5253 it ( "prevents bare relative link navigation without treating it as a file" , ( ) => {
@@ -89,7 +90,7 @@ describe("Markdown file-like href behavior", () => {
8990 } ) ;
9091 fireEvent ( link as Element , clickEvent ) ;
9192 expect ( clickEvent . defaultPrevented ) . toBe ( true ) ;
92- expect ( onOpenFileLink ) . toHaveBeenCalledWith ( "/workspace/src/example.ts" ) ;
93+ expectOpenedFileTarget ( onOpenFileLink , "/workspace/src/example.ts" ) ;
9394 } ) ;
9495
9596 it ( "still intercepts dotless workspace file hrefs when a file opener is provided" , ( ) => {
@@ -112,7 +113,7 @@ describe("Markdown file-like href behavior", () => {
112113 } ) ;
113114 fireEvent ( link as Element , clickEvent ) ;
114115 expect ( clickEvent . defaultPrevented ) . toBe ( true ) ;
115- expect ( onOpenFileLink ) . toHaveBeenCalledWith ( "/workspace/CodexMonitor/LICENSE" ) ;
116+ expectOpenedFileTarget ( onOpenFileLink , "/workspace/CodexMonitor/LICENSE" ) ;
116117 } ) ;
117118
118119 it ( "intercepts mounted workspace links outside the old root allowlist" , ( ) => {
@@ -135,7 +136,7 @@ describe("Markdown file-like href behavior", () => {
135136 } ) ;
136137 fireEvent ( link as Element , clickEvent ) ;
137138 expect ( clickEvent . defaultPrevented ) . toBe ( true ) ;
138- expect ( onOpenFileLink ) . toHaveBeenCalledWith ( "/workspace/.github/workflows" ) ;
139+ expectOpenedFileTarget ( onOpenFileLink , "/workspace/.github/workflows" ) ;
139140 } ) ;
140141
141142 it ( "intercepts mounted workspace directory links that resolve relative to the workspace" , ( ) => {
@@ -158,20 +159,43 @@ describe("Markdown file-like href behavior", () => {
158159 } ) ;
159160 fireEvent ( link as Element , clickEvent ) ;
160161 expect ( clickEvent . defaultPrevented ) . toBe ( true ) ;
161- expect ( onOpenFileLink ) . toHaveBeenCalledWith ( "/workspace/dist/assets" ) ;
162+ expectOpenedFileTarget ( onOpenFileLink , "/workspace/dist/assets" ) ;
162163 } ) ;
163164
164- it ( "keeps generic workspace routes as normal markdown links" , ( ) => {
165+ it ( "keeps exact workspace routes as normal markdown links" , ( ) => {
165166 const onOpenFileLink = vi . fn ( ) ;
166167 render (
167168 < Markdown
168- value = "See [overview ](/workspace/reviews/overview )"
169+ value = "See [reviews ](/workspace/reviews)"
169170 className = "markdown"
170171 workspacePath = "/Users/sotiriskaniras/Documents/Development/Forks/CodexMonitor"
171172 onOpenFileLink = { onOpenFileLink }
172173 /> ,
173174 ) ;
174175
176+ const link = screen . getByText ( "reviews" ) . closest ( "a" ) ;
177+ expect ( link ?. getAttribute ( "href" ) ) . toBe ( "/workspace/reviews" ) ;
178+
179+ const clickEvent = createEvent . click ( link as Element , {
180+ bubbles : true ,
181+ cancelable : true ,
182+ } ) ;
183+ fireEvent ( link as Element , clickEvent ) ;
184+ expect ( clickEvent . defaultPrevented ) . toBe ( true ) ;
185+ expect ( onOpenFileLink ) . not . toHaveBeenCalled ( ) ;
186+ } ) ;
187+
188+ it ( "keeps nested workspace reviews routes local even when the workspace basename matches" , ( ) => {
189+ const onOpenFileLink = vi . fn ( ) ;
190+ render (
191+ < Markdown
192+ value = "See [overview](/workspace/reviews/overview)"
193+ className = "markdown"
194+ workspacePath = "/Users/sotiriskaniras/Documents/Development/Forks/reviews"
195+ onOpenFileLink = { onOpenFileLink }
196+ /> ,
197+ ) ;
198+
175199 const link = screen . getByText ( "overview" ) . closest ( "a" ) ;
176200 expect ( link ?. getAttribute ( "href" ) ) . toBe ( "/workspace/reviews/overview" ) ;
177201
@@ -207,6 +231,29 @@ describe("Markdown file-like href behavior", () => {
207231 expect ( onOpenFileLink ) . not . toHaveBeenCalled ( ) ;
208232 } ) ;
209233
234+ it ( "keeps nested reviews routes local even when the workspace basename matches the route segment" , ( ) => {
235+ const onOpenFileLink = vi . fn ( ) ;
236+ render (
237+ < Markdown
238+ value = "See [overview](/workspaces/team/reviews/overview)"
239+ className = "markdown"
240+ workspacePath = "/Users/sotiriskaniras/Documents/Development/Forks/reviews"
241+ onOpenFileLink = { onOpenFileLink }
242+ /> ,
243+ ) ;
244+
245+ const link = screen . getByText ( "overview" ) . closest ( "a" ) ;
246+ expect ( link ?. getAttribute ( "href" ) ) . toBe ( "/workspaces/team/reviews/overview" ) ;
247+
248+ const clickEvent = createEvent . click ( link as Element , {
249+ bubbles : true ,
250+ cancelable : true ,
251+ } ) ;
252+ fireEvent ( link as Element , clickEvent ) ;
253+ expect ( clickEvent . defaultPrevented ) . toBe ( true ) ;
254+ expect ( onOpenFileLink ) . not . toHaveBeenCalled ( ) ;
255+ } ) ;
256+
210257 it ( "still intercepts nested workspace file hrefs when a file opener is provided" , ( ) => {
211258 const onOpenFileLink = vi . fn ( ) ;
212259 render (
@@ -227,7 +274,30 @@ describe("Markdown file-like href behavior", () => {
227274 } ) ;
228275 fireEvent ( link as Element , clickEvent ) ;
229276 expect ( clickEvent . defaultPrevented ) . toBe ( true ) ;
230- expect ( onOpenFileLink ) . toHaveBeenCalledWith ( "/workspaces/team/CodexMonitor/src" ) ;
277+ expectOpenedFileTarget ( onOpenFileLink , "/workspaces/team/CodexMonitor/src" ) ;
278+ } ) ;
279+
280+ it ( "treats extensionless paths under /workspace/settings as files" , ( ) => {
281+ const onOpenFileLink = vi . fn ( ) ;
282+ render (
283+ < Markdown
284+ value = "See [license](/workspace/settings/LICENSE)"
285+ className = "markdown"
286+ workspacePath = "/Users/sotiriskaniras/Documents/Development/Forks/settings"
287+ onOpenFileLink = { onOpenFileLink }
288+ /> ,
289+ ) ;
290+
291+ const link = screen . getByText ( "license" ) . closest ( "a" ) ;
292+ expect ( link ?. getAttribute ( "href" ) ) . toBe ( "/workspace/settings/LICENSE" ) ;
293+
294+ const clickEvent = createEvent . click ( link as Element , {
295+ bubbles : true ,
296+ cancelable : true ,
297+ } ) ;
298+ fireEvent ( link as Element , clickEvent ) ;
299+ expect ( clickEvent . defaultPrevented ) . toBe ( true ) ;
300+ expectOpenedFileTarget ( onOpenFileLink , "/workspace/settings/LICENSE" ) ;
231301 } ) ;
232302
233303 it ( "intercepts file hrefs that use #L line anchors" , ( ) => {
@@ -249,7 +319,52 @@ describe("Markdown file-like href behavior", () => {
249319 } ) ;
250320 fireEvent ( link as Element , clickEvent ) ;
251321 expect ( clickEvent . defaultPrevented ) . toBe ( true ) ;
252- expect ( onOpenFileLink ) . toHaveBeenCalledWith ( "./docs/setup.md:12" ) ;
322+ expectOpenedFileTarget ( onOpenFileLink , "./docs/setup.md" , 12 ) ;
323+ } ) ;
324+
325+ it ( "intercepts Windows absolute file hrefs with #L anchors and preserves the tooltip" , ( ) => {
326+ const onOpenFileLink = vi . fn ( ) ;
327+ const onOpenFileLinkMenu = vi . fn ( ) ;
328+ const linkedPath =
329+ "I:\\gpt-projects\\CodexMonitor\\src\\features\\settings\\components\\sections\\SettingsDisplaySection.tsx#L422" ;
330+ render (
331+ < Markdown
332+ value = { `See [SettingsDisplaySection.tsx](${ linkedPath } )` }
333+ className = "markdown"
334+ onOpenFileLink = { onOpenFileLink }
335+ onOpenFileLinkMenu = { onOpenFileLinkMenu }
336+ /> ,
337+ ) ;
338+
339+ const link = screen . getByText ( "SettingsDisplaySection.tsx" ) . closest ( "a" ) ;
340+ expect ( link ?. getAttribute ( "href" ) ) . toBe (
341+ "I:%5Cgpt-projects%5CCodexMonitor%5Csrc%5Cfeatures%5Csettings%5Ccomponents%5Csections%5CSettingsDisplaySection.tsx#L422" ,
342+ ) ;
343+ expect ( link ?. getAttribute ( "title" ) ) . toBe (
344+ "I:\\gpt-projects\\CodexMonitor\\src\\features\\settings\\components\\sections\\SettingsDisplaySection.tsx:422" ,
345+ ) ;
346+
347+ const clickEvent = createEvent . click ( link as Element , {
348+ bubbles : true ,
349+ cancelable : true ,
350+ } ) ;
351+ fireEvent ( link as Element , clickEvent ) ;
352+ expect ( clickEvent . defaultPrevented ) . toBe ( true ) ;
353+ expectOpenedFileTarget (
354+ onOpenFileLink ,
355+ "I:\\gpt-projects\\CodexMonitor\\src\\features\\settings\\components\\sections\\SettingsDisplaySection.tsx" ,
356+ 422 ,
357+ ) ;
358+
359+ fireEvent . contextMenu ( link as Element ) ;
360+ expect ( onOpenFileLinkMenu ) . toHaveBeenCalledWith (
361+ expect . anything ( ) ,
362+ {
363+ path : "I:\\gpt-projects\\CodexMonitor\\src\\features\\settings\\components\\sections\\SettingsDisplaySection.tsx" ,
364+ line : 422 ,
365+ column : null ,
366+ } ,
367+ ) ;
253368 } ) ;
254369
255370 it ( "prevents unsupported route fragments without treating them as file links" , ( ) => {
@@ -274,42 +389,133 @@ describe("Markdown file-like href behavior", () => {
274389 expect ( onOpenFileLink ) . not . toHaveBeenCalled ( ) ;
275390 } ) ;
276391
277- it ( "does not turn natural-language slash phrases into file links" , ( ) => {
392+ it ( "keeps workspace settings #L anchors as local routes" , ( ) => {
393+ const onOpenFileLink = vi . fn ( ) ;
394+ render (
395+ < Markdown
396+ value = "See [settings](/workspace/settings#L12)"
397+ className = "markdown"
398+ workspacePath = "/Users/sotiriskaniras/Documents/Development/Forks/CodexMonitor"
399+ onOpenFileLink = { onOpenFileLink }
400+ /> ,
401+ ) ;
402+
403+ const link = screen . getByText ( "settings" ) . closest ( "a" ) ;
404+ expect ( link ?. getAttribute ( "href" ) ) . toBe ( "/workspace/settings#L12" ) ;
405+
406+ const clickEvent = createEvent . click ( link as Element , {
407+ bubbles : true ,
408+ cancelable : true ,
409+ } ) ;
410+ fireEvent ( link as Element , clickEvent ) ;
411+ expect ( clickEvent . defaultPrevented ) . toBe ( true ) ;
412+ expect ( onOpenFileLink ) . not . toHaveBeenCalled ( ) ;
413+ } ) ;
414+
415+ it ( "keeps workspace reviews #L anchors as local routes" , ( ) => {
416+ const onOpenFileLink = vi . fn ( ) ;
417+ render (
418+ < Markdown
419+ value = "See [reviews](/workspace/reviews#L9)"
420+ className = "markdown"
421+ workspacePath = "/Users/sotiriskaniras/Documents/Development/Forks/CodexMonitor"
422+ onOpenFileLink = { onOpenFileLink }
423+ /> ,
424+ ) ;
425+
426+ const link = screen . getByText ( "reviews" ) . closest ( "a" ) ;
427+ expect ( link ?. getAttribute ( "href" ) ) . toBe ( "/workspace/reviews#L9" ) ;
428+
429+ const clickEvent = createEvent . click ( link as Element , {
430+ bubbles : true ,
431+ cancelable : true ,
432+ } ) ;
433+ fireEvent ( link as Element , clickEvent ) ;
434+ expect ( clickEvent . defaultPrevented ) . toBe ( true ) ;
435+ expect ( onOpenFileLink ) . not . toHaveBeenCalled ( ) ;
436+ } ) ;
437+
438+ it ( "does not linkify workspace settings #L anchors in plain text" , ( ) => {
278439 const { container } = render (
279440 < Markdown
280- value = "Keep the current app/daemon behavior and the existing Git/Plan experience ."
441+ value = "See /workspace/settings#L12 for app settings ."
281442 className = "markdown"
443+ workspacePath = "/Users/sotiriskaniras/Documents/Development/Forks/CodexMonitor"
282444 /> ,
283445 ) ;
284446
285447 expect ( container . querySelector ( ".message-file-link" ) ) . toBeNull ( ) ;
286- expect ( container . textContent ) . toContain ( "app/daemon" ) ;
287- expect ( container . textContent ) . toContain ( "Git/Plan" ) ;
448+ expect ( container . textContent ) . toContain ( "/workspace/settings#L12" ) ;
288449 } ) ;
289450
290- it ( "does not turn longer slash phrases into file links " , ( ) => {
451+ it ( "does not linkify Windows file paths embedded in custom URIs " , ( ) => {
291452 const { container } = render (
292453 < Markdown
293- value = "This keeps Spec/Verification/Evidence in the note without turning it into a file link ."
454+ value = "Open vscode://file/C:/repo/src/App.tsx:12 in VS Code ."
294455 className = "markdown"
295456 /> ,
296457 ) ;
297458
298459 expect ( container . querySelector ( ".message-file-link" ) ) . toBeNull ( ) ;
299- expect ( container . textContent ) . toContain ( "Spec/Verification/Evidence " ) ;
460+ expect ( container . textContent ) . toContain ( "vscode://file/C:/repo/src/App.tsx:12 " ) ;
300461 } ) ;
301462
302- it ( "still turns clear file paths in plain text into file links" , ( ) => {
463+ it ( "does not turn workspace review #L anchors in inline code into file links" , ( ) => {
303464 const { container } = render (
304465 < Markdown
305- value = "See docs/setup.md and /Users/example/project/src/index.ts for details ."
466+ value = "Use `/workspace/reviews#L9` to reference the reviews route ."
306467 className = "markdown"
468+ workspacePath = "/Users/sotiriskaniras/Documents/Development/Forks/CodexMonitor"
469+ /> ,
470+ ) ;
471+
472+ expect ( container . querySelector ( ".message-file-link" ) ) . toBeNull ( ) ;
473+ expect ( container . querySelector ( "code" ) ?. textContent ) . toBe ( "/workspace/reviews#L9" ) ;
474+ } ) ;
475+
476+ it ( "still opens mounted file links when the workspace basename is settings" , ( ) => {
477+ const onOpenFileLink = vi . fn ( ) ;
478+ render (
479+ < Markdown
480+ value = "See [app](/workspace/settings/src/App.tsx)"
481+ className = "markdown"
482+ onOpenFileLink = { onOpenFileLink }
307483 /> ,
308484 ) ;
309485
310- const fileLinks = [ ...container . querySelectorAll ( ".message-file-link" ) ] ;
311- expect ( fileLinks ) . toHaveLength ( 2 ) ;
312- expect ( fileLinks [ 0 ] ?. textContent ) . toContain ( "setup.md" ) ;
313- expect ( fileLinks [ 1 ] ?. textContent ) . toContain ( "index.ts" ) ;
486+ const link = screen . getByText ( "app" ) . closest ( "a" ) ;
487+ expect ( link ?. getAttribute ( "href" ) ) . toBe ( "/workspace/settings/src/App.tsx" ) ;
488+
489+ const clickEvent = createEvent . click ( link as Element , {
490+ bubbles : true ,
491+ cancelable : true ,
492+ } ) ;
493+ fireEvent ( link as Element , clickEvent ) ;
494+ expect ( clickEvent . defaultPrevented ) . toBe ( true ) ;
495+ expectOpenedFileTarget ( onOpenFileLink , "/workspace/settings/src/App.tsx" ) ;
314496 } ) ;
497+
498+ it ( "keeps nested settings routes local when the workspace basename is settings" , ( ) => {
499+ const onOpenFileLink = vi . fn ( ) ;
500+ render (
501+ < Markdown
502+ value = "See [profile](/workspace/settings/profile)"
503+ className = "markdown"
504+ workspacePath = "/Users/sotiriskaniras/Documents/Development/Forks/settings"
505+ onOpenFileLink = { onOpenFileLink }
506+ /> ,
507+ ) ;
508+
509+ const link = screen . getByText ( "profile" ) . closest ( "a" ) ;
510+ expect ( link ?. getAttribute ( "href" ) ) . toBe ( "/workspace/settings/profile" ) ;
511+
512+ const clickEvent = createEvent . click ( link as Element , {
513+ bubbles : true ,
514+ cancelable : true ,
515+ } ) ;
516+ fireEvent ( link as Element , clickEvent ) ;
517+ expect ( clickEvent . defaultPrevented ) . toBe ( true ) ;
518+ expect ( onOpenFileLink ) . not . toHaveBeenCalled ( ) ;
519+ } ) ;
520+
315521} ) ;
0 commit comments