@@ -80,8 +80,8 @@ function startStaticServer() {
8080 const url = new URL ( request . url , "http://127.0.0.1" ) ;
8181 let pathname = decodeURIComponent ( url . pathname ) ;
8282 if ( pathname === "/" ) pathname = "/index.html" ;
83- if ( / ^ \/ e v e n t s \/ [ ^ / ] + $ / . test ( pathname ) ) pathname = "/event.html" ;
84- if ( / ^ \/ m e m b e r s \/ [ ^ / ] + $ / . test ( pathname ) ) pathname = "/member.html" ;
83+ if ( / ^ \/ e v e n t s \/ [ ^ / ] + $ / . test ( pathname ) && ! path . extname ( pathname ) ) pathname = "/event.html" ;
84+ if ( / ^ \/ m e m b e r s \/ [ ^ / ] + $ / . test ( pathname ) && ! path . extname ( pathname ) ) pathname = "/member.html" ;
8585 if ( pathname === "/event" ) pathname = "/event.html" ;
8686 if ( pathname === "/member" ) pathname = "/member.html" ;
8787
@@ -380,6 +380,88 @@ async function main() {
380380 } ) ;
381381 }
382382
383+ async function auditCurrentPageInternalTargets ( label ) {
384+ const broken = await evaluateFunction ( async ( ) => {
385+ const blockedProtocols = / ^ ( b l o b | d a t a | j a v a s c r i p t | m a i l t o | s m s | t e l ) : / i;
386+ const seen = new Set ( ) ;
387+ const targets = [ ] ;
388+ const currentWithoutHash = new URL ( window . location . href ) ;
389+ currentWithoutHash . hash = "" ;
390+
391+ function addTarget ( node , attribute , type ) {
392+ const raw = node . getAttribute ( attribute ) ;
393+ if ( ! raw || blockedProtocols . test ( raw . trim ( ) ) ) return ;
394+
395+ let url ;
396+ try {
397+ url = new URL ( raw , document . baseURI ) ;
398+ } catch ( error ) {
399+ targets . push ( { error : error . message , raw, type } ) ;
400+ return ;
401+ }
402+
403+ if ( url . origin !== window . location . origin ) return ;
404+
405+ const hash = url . hash ;
406+ url . hash = "" ;
407+ const href = url . toString ( ) ;
408+ const key = `${ type } :${ href } :${ hash } ` ;
409+ if ( seen . has ( key ) ) return ;
410+ seen . add ( key ) ;
411+ targets . push ( { href, hash, raw, type } ) ;
412+ }
413+
414+ [
415+ [ "a[href]" , "href" , "anchor" ] ,
416+ [ "area[href]" , "href" , "area" ] ,
417+ [ "form[action]" , "action" , "form" ] ,
418+ [ "iframe[src]" , "src" , "iframe" ] ,
419+ [ "img[src]" , "src" , "image" ] ,
420+ [ "link[href]" , "href" , "link" ] ,
421+ [ "script[src]" , "src" , "script" ] ,
422+ [ "source[src]" , "src" , "source" ] ,
423+ [ "audio[src]" , "src" , "audio" ] ,
424+ [ "video[src]" , "src" , "video" ]
425+ ] . forEach ( ( [ selector , attribute , type ] ) => {
426+ document . querySelectorAll ( selector ) . forEach ( ( node ) => addTarget ( node , attribute , type ) ) ;
427+ } ) ;
428+
429+ const failures = [ ] ;
430+ for ( const target of targets ) {
431+ if ( target . error ) {
432+ failures . push ( target ) ;
433+ continue ;
434+ }
435+
436+ if ( target . hash && target . href === currentWithoutHash . toString ( ) ) {
437+ const id = decodeURIComponent ( target . hash . slice ( 1 ) ) ;
438+ if ( id && ! document . getElementById ( id ) && document . getElementsByName ( id ) . length === 0 ) {
439+ failures . push ( { ...target , error : `Missing fragment target ${ target . hash } ` } ) ;
440+ }
441+ }
442+
443+ let response ;
444+ try {
445+ response = await fetch ( target . href , { cache : "no-store" , method : "HEAD" } ) ;
446+ if ( response . status === 405 ) {
447+ response = await fetch ( target . href , { cache : "no-store" , method : "GET" } ) ;
448+ }
449+ } catch ( error ) {
450+ failures . push ( { ...target , error : error . message } ) ;
451+ continue ;
452+ }
453+
454+ if ( ! response . ok ) {
455+ failures . push ( { ...target , status : response . status } ) ;
456+ }
457+ }
458+
459+ return failures ;
460+ } ) ;
461+
462+ assert . deepEqual ( broken , [ ] , `${ label } should not expose broken internal links or assets.` ) ;
463+ }
464+
383465 try {
384466 await cdp . ready ;
385467 const { targetId } = await cdp . send ( "Target.createTarget" , { url : "about:blank" } ) ;
@@ -469,6 +551,7 @@ async function main() {
469551
470552 return true ;
471553 } ) ;
554+ await auditCurrentPageInternalTargets ( "home page" ) ;
472555
473556 await navigate ( "/events/santa-monica-beach-cleanup" , "#event-page" ) ;
474557 await evaluateFlow ( async ( helpers ) => {
@@ -478,6 +561,7 @@ async function main() {
478561 assertBrowser ( text ( "#event-page" ) . includes ( "Example event" ) , "Seed event pages should be marked as examples." ) ;
479562 return true ;
480563 } ) ;
564+ await auditCurrentPageInternalTargets ( "clean event page" ) ;
481565
482566 await navigate ( "/event.html?id=santa-monica-beach-cleanup" , "#event-page" ) ;
483567 await evaluateFlow ( async ( helpers ) => {
@@ -516,6 +600,7 @@ async function main() {
516600
517601 return true ;
518602 } ) ;
603+ await auditCurrentPageInternalTargets ( "event join page" ) ;
519604
520605 await navigate ( "/index.html#account" , "#account-dashboard" ) ;
521606 await evaluateFlow ( async ( helpers ) => {
@@ -534,6 +619,34 @@ async function main() {
534619 assertBrowser ( text ( "#member-page" ) . includes ( "Santa Monica Beach Cleanup" ) , "Member profile should list joined event activity." ) ;
535620 return true ;
536621 } ) ;
622+ await auditCurrentPageInternalTargets ( "member query page" ) ;
623+
624+ await navigate ( `/members/${ memberId ( "riley@example.com" ) } ` , "#member-page" ) ;
625+ await evaluateFlow ( async ( helpers ) => {
626+ const { assertBrowser, text, waitUntil } = helpers ;
627+ await waitUntil ( ( ) => text ( "#member-page" ) . includes ( "Riley Stone" ) , "clean member profile" ) ;
628+ assertBrowser ( text ( "#member-page" ) . includes ( "Santa Monica Beach Cleanup" ) , "Clean member URL should list joined event activity." ) ;
629+ return true ;
630+ } ) ;
631+ await auditCurrentPageInternalTargets ( "clean member page" ) ;
632+
633+ await navigate ( "/events/not-a-real-event" , "#event-page" ) ;
634+ await evaluateFlow ( async ( helpers ) => {
635+ const { assertBrowser, text } = helpers ;
636+ assertBrowser ( text ( "#event-page" ) . includes ( "Event not found" ) , "Missing clean event URLs should render the not-found state." ) ;
637+ assertBrowser ( Boolean ( document . querySelector ( 'a[href="/#events"]' ) ) , "Missing event links should return to the root events section." ) ;
638+ return true ;
639+ } ) ;
640+ await auditCurrentPageInternalTargets ( "event not-found page" ) ;
641+
642+ await navigate ( `/members/${ memberId ( "missing@example.com" ) } ` , "#member-page" ) ;
643+ await evaluateFlow ( async ( helpers ) => {
644+ const { assertBrowser, text } = helpers ;
645+ assertBrowser ( text ( "#member-page" ) . includes ( "Member not found" ) , "Missing clean member URLs should render the not-found state." ) ;
646+ assertBrowser ( Boolean ( document . querySelector ( 'a[href="/#account"]' ) ) , "Missing member links should return to the root account section." ) ;
647+ return true ;
648+ } ) ;
649+ await auditCurrentPageInternalTargets ( "member not-found page" ) ;
537650
538651 await navigate ( "/event.html?id=santa-monica-beach-cleanup" , "#event-page" ) ;
539652 await evaluateFlow ( async ( helpers ) => {
0 commit comments