@@ -16,7 +16,7 @@ type CapturedClickEvent = {
1616 altKey ?: boolean ;
1717 button : number ;
1818 ctrlKey ?: boolean ;
19- currentTarget : { target : string } ;
19+ currentTarget : { hasAttribute ( name : string ) : boolean ; target : string } ;
2020 defaultPrevented : boolean ;
2121 metaKey ?: boolean ;
2222 preventDefault ( ) : void ;
@@ -354,7 +354,7 @@ describe("Link App Router navigation scheduling", () => {
354354
355355 const clickEvent = {
356356 button : 0 ,
357- currentTarget : { target : "" } ,
357+ currentTarget : { hasAttribute : ( ) => false , target : "" } ,
358358 defaultPrevented : false ,
359359 preventDefault ( ) {
360360 this . defaultPrevented = true ;
@@ -372,6 +372,77 @@ describe("Link App Router navigation scheduling", () => {
372372 expect ( navigate ) . toHaveBeenCalledWith ( "/target" , 0 , "navigate" , "push" , undefined , true ) ;
373373 expect ( transitionStates ) . toEqual ( [ true ] ) ;
374374 } ) ;
375+
376+ it ( "lets the browser handle download links without app-router navigation" , async ( ) => {
377+ vi . resetModules ( ) ;
378+
379+ let capturedAnchorProps : CapturedAnchorProps | undefined ;
380+ const startTransition = vi . fn ( ( callback : ( ) => void ) => {
381+ callback ( ) ;
382+ } ) ;
383+
384+ const captureAnchor = ( type : unknown , props : unknown ) => {
385+ if ( type === "a" && props !== null && typeof props === "object" ) {
386+ capturedAnchorProps = props ;
387+ }
388+ } ;
389+
390+ mockReactAnchorCaptureForLinkOnly_DO_NOT_REUSE ( { captureAnchor, startTransition } ) ;
391+
392+ const navigate = vi . fn ( async ( ) => { } ) ;
393+ vi . stubGlobal ( "window" , {
394+ __VINEXT_RSC_NAVIGATE__ : navigate ,
395+ addEventListener : vi . fn ( ) ,
396+ history : {
397+ pushState : vi . fn ( ) ,
398+ replaceState : vi . fn ( ) ,
399+ } ,
400+ location : {
401+ href : "https://example.com/current" ,
402+ origin : "https://example.com" ,
403+ } ,
404+ scrollTo : vi . fn ( ) ,
405+ } ) ;
406+
407+ const { default : IsolatedLink } = await import ( "../packages/vinext/src/shims/link.js" ) ;
408+ const React = await vi . importActual < typeof import ( "react" ) > ( "react" ) ;
409+ const onClick = vi . fn ( ) ;
410+ const onNavigate = vi . fn ( ) ;
411+
412+ // Ported from Next.js: test/e2e/link-on-navigate-prop/index.test.ts
413+ // https://github.com/vercel/next.js/blob/canary/test/e2e/link-on-navigate-prop/index.test.ts
414+ ReactDOMServer . renderToString (
415+ React . createElement (
416+ IsolatedLink ,
417+ { download : true , href : "/file.pdf" , onClick, onNavigate, prefetch : false } ,
418+ "download" ,
419+ ) ,
420+ ) ;
421+
422+ const clickEvent = {
423+ button : 0 ,
424+ currentTarget : {
425+ hasAttribute : ( name : string ) => name === "download" ,
426+ target : "" ,
427+ } ,
428+ defaultPrevented : false ,
429+ preventDefault ( ) {
430+ this . defaultPrevented = true ;
431+ } ,
432+ } ;
433+ const linkOnClick = capturedAnchorProps ?. onClick ;
434+ expect ( linkOnClick ) . toBeTypeOf ( "function" ) ;
435+ if ( linkOnClick === undefined ) {
436+ throw new Error ( "Expected rendered Link anchor to expose an onClick handler" ) ;
437+ }
438+ await linkOnClick ( clickEvent ) ;
439+
440+ expect ( onClick ) . toHaveBeenCalledTimes ( 1 ) ;
441+ expect ( clickEvent . defaultPrevented ) . toBe ( false ) ;
442+ expect ( onNavigate ) . not . toHaveBeenCalled ( ) ;
443+ expect ( startTransition ) . not . toHaveBeenCalled ( ) ;
444+ expect ( navigate ) . not . toHaveBeenCalled ( ) ;
445+ } ) ;
375446} ) ;
376447
377448async function renderIsolatedLink ( options : {
0 commit comments