@@ -5,7 +5,14 @@ import React, { StrictMode } from 'react';
55import { defaultRegistry } from 'react-sweet-state' ;
66
77import { isServerEnvironment } from '../common/utils/is-server-environment' ;
8- import { Route , RouteComponent , Router , type Plugin } from '../index' ;
8+ import {
9+ Route ,
10+ RouteComponent ,
11+ Router ,
12+ type Plugin ,
13+ usePathParam ,
14+ useQueryParam ,
15+ } from '../index' ;
916
1017jest . mock ( '../common/utils/is-server-environment' ) ;
1118
@@ -318,5 +325,209 @@ describe('<Router /> client-side integration tests', () => {
318325 expect ( screen . getByText ( 'route component' ) ) . toBeInTheDocument ( ) ;
319326 } ) ;
320327 } ) ;
328+
329+ describe ( `path matching integration tests: strict mode ${ strictModeState } ` , ( ) => {
330+ it ( 'matches dynamic route with optional parameter' , ( ) => {
331+ const MigrationComponent = ( ) => {
332+ const [ step ] = usePathParam ( 'step' ) ;
333+ const [ migrationId ] = usePathParam ( 'migrationId' ) ;
334+
335+ return (
336+ < div >
337+ Step: { step } , Migration ID: { migrationId || 'N/A' }
338+ </ div >
339+ ) ;
340+ } ;
341+
342+ const route = {
343+ name : 'migration' ,
344+ path : '/settings/system/migration/:step/:migrationId?' ,
345+ component : MigrationComponent ,
346+ } ;
347+ const { history } = mountRouter ( { routes : [ route ] , strictMode : true } ) ;
348+
349+ act ( ( ) => {
350+ history . push ( '/settings/system/migration/plan-configuration/123' ) ;
351+ } ) ;
352+
353+ expect (
354+ screen . getByText ( 'Step: plan-configuration, Migration ID: 123' )
355+ ) . toBeInTheDocument ( ) ;
356+ } ) ;
357+
358+ it ( 'matches route with regex constraint on path parameter' , ( ) => {
359+ const PlanComponent = ( ) => {
360+ const [ planId ] = usePathParam ( 'planId' ) ;
361+
362+ return < div > Plan ID: { planId } </ div > ;
363+ } ;
364+
365+ const route = {
366+ name : 'plans' ,
367+ path : '/plans/:planId(\\d+)' ,
368+ component : PlanComponent ,
369+ } ;
370+ const { history } = mountRouter ( { routes : [ route ] , strictMode : true } ) ;
371+
372+ act ( ( ) => {
373+ history . push ( '/plans/456' ) ;
374+ } ) ;
375+
376+ expect ( screen . getByText ( 'Plan ID: 456' ) ) . toBeInTheDocument ( ) ;
377+ } ) ;
378+
379+ it ( 'matches route with multiple dynamic parameters' , ( ) => {
380+ const ProjectAppComponent = ( ) => {
381+ const [ projectType ] = usePathParam ( 'projectType' ) ;
382+ const [ projectKey ] = usePathParam ( 'projectKey' ) ;
383+ const [ appId ] = usePathParam ( 'appId' ) ;
384+
385+ return (
386+ < div >
387+ Project Type: { projectType } , Project Key: { projectKey } , App ID:{ ' ' }
388+ { appId }
389+ </ div >
390+ ) ;
391+ } ;
392+
393+ const route = {
394+ name : 'project-app' ,
395+ path : '/app/:projectType(software|servicedesk)/projects/:projectKey/apps/:appId' ,
396+ component : ProjectAppComponent ,
397+ } ;
398+ const { history } = mountRouter ( { routes : [ route ] , strictMode : true } ) ;
399+
400+ act ( ( ) => {
401+ history . push ( '/app/software/projects/PROJ123/apps/456' ) ;
402+ } ) ;
403+
404+ expect (
405+ screen . getByText (
406+ 'Project Type: software, Project Key: PROJ123, App ID: 456'
407+ )
408+ ) . toBeInTheDocument ( ) ;
409+ } ) ;
410+
411+ it ( 'matches route with dynamic and query parameters' , ( ) => {
412+ const IssueComponent = ( ) => {
413+ const [ issueKey ] = usePathParam ( 'issueKey' ) ;
414+ const [ queryParam ] = useQueryParam ( 'query' ) ;
415+
416+ return (
417+ < div >
418+ Issue Key: { issueKey } , Query: { queryParam || 'None' }
419+ </ div >
420+ ) ;
421+ } ;
422+
423+ const route = {
424+ name : 'browse' ,
425+ path : '/browse/:issueKey(\\w+-\\d+)' ,
426+ component : IssueComponent ,
427+ } ;
428+ const { history } = mountRouter ( { routes : [ route ] , strictMode : true } ) ;
429+
430+ act ( ( ) => {
431+ history . push ( '/browse/ISSUE-123?query=details' ) ;
432+ } ) ;
433+
434+ expect (
435+ screen . getByText ( 'Issue Key: ISSUE-123, Query: details' )
436+ ) . toBeInTheDocument ( ) ;
437+ } ) ;
438+
439+ it ( 'matches route with complex regex constraint on path parameter and wildcard' , ( ) => {
440+ const IssueComponent = ( ) => {
441+ const [ issueKey ] = usePathParam ( 'issueKey' ) ;
442+
443+ return < div > Issue Key: { issueKey } </ div > ;
444+ } ;
445+
446+ const route = {
447+ name : 'browse' ,
448+ path : '/browse/:issueKey(\\w+-\\d+)(.*)?' ,
449+ component : IssueComponent ,
450+ } ;
451+ const { history } = mountRouter ( { routes : [ route ] , strictMode : true } ) ;
452+
453+ act ( ( ) => {
454+ history . push ( '/browse/ISSUE-123/details' ) ;
455+ } ) ;
456+
457+ expect ( screen . getByText ( 'Issue Key: ISSUE-123' ) ) . toBeInTheDocument ( ) ;
458+ } ) ;
459+
460+ it ( 'matches route with multiple dynamic segments and regex constraints' , ( ) => {
461+ const SettingsComponent = ( ) => {
462+ const [ settingsType ] = usePathParam ( 'settingsType' ) ;
463+ const [ appId ] = usePathParam ( 'appId' ) ;
464+ const [ envId ] = usePathParam ( 'envId' ) ;
465+ const [ route ] = usePathParam ( 'route' ) ;
466+
467+ return (
468+ < div >
469+ Settings Type: { settingsType } , App ID: { appId } , Environment ID:{ ' ' }
470+ { envId } , Route: { route || 'None' }
471+ </ div >
472+ ) ;
473+ } ;
474+
475+ const route = {
476+ name : 'settings' ,
477+ path : '/settings/apps/:settingsType(configure|get-started)/:appId/:envId/:route?' ,
478+ component : SettingsComponent ,
479+ } ;
480+ const { history } = mountRouter ( { routes : [ route ] , strictMode : true } ) ;
481+
482+ act ( ( ) => {
483+ history . push ( '/settings/apps/configure/app123/env456/setup' ) ;
484+ } ) ;
485+
486+ expect (
487+ screen . getByText (
488+ 'Settings Type: configure, App ID: app123, Environment ID: env456, Route: setup'
489+ )
490+ ) . toBeInTheDocument ( ) ;
491+ } ) ;
492+
493+ it ( 'matches route with regex constraint and renders wildcard route for invalid paths' , ( ) => {
494+ const IssueComponent = ( ) => {
495+ const [ issueKey ] = usePathParam ( 'issueKey' ) ;
496+
497+ return < div > Issue Key: { issueKey } </ div > ;
498+ } ;
499+
500+ const NotFoundComponent = ( ) => < div > Not Found</ div > ;
501+
502+ const routes = [
503+ {
504+ name : 'issue' ,
505+ path : '/browse/:issueKey(\\w+-\\d+)(.*)?' ,
506+ component : IssueComponent ,
507+ } ,
508+ {
509+ name : 'wildcard' ,
510+ path : '/' ,
511+ component : NotFoundComponent ,
512+ } ,
513+ ] ;
514+ const { history } = mountRouter ( { routes, strictMode : true } ) ;
515+
516+ act ( ( ) => {
517+ history . push ( '/browse/TEST-1' ) ;
518+ } ) ;
519+ expect ( screen . getByText ( 'Issue Key: TEST-1' ) ) . toBeInTheDocument ( ) ;
520+
521+ act ( ( ) => {
522+ history . push ( '/browse/1' ) ;
523+ } ) ;
524+ expect ( screen . getByText ( 'Not Found' ) ) . toBeInTheDocument ( ) ;
525+
526+ act ( ( ) => {
527+ history . push ( '/browse/TEST' ) ;
528+ } ) ;
529+ expect ( screen . getByText ( 'Not Found' ) ) . toBeInTheDocument ( ) ;
530+ } ) ;
531+ } ) ;
321532 }
322533} ) ;
0 commit comments