@@ -245,10 +245,22 @@ describe('findRouteMatch', () => {
245245 const tree = makeTree ( [ '/$a/b/c' , '/a/$' ] )
246246 expect ( findRouteMatch ( '/a/b/c' , tree ) ?. route . id ) . toBe ( '/a/$' )
247247 } )
248+ it ( 'case-insensitive static+wildcard beats optional+static' , ( ) => {
249+ const tree = makeTree ( [ '/{-$a}/bar' , '/foo/$' ] )
250+ expect ( findRouteMatch ( '/FOO/bar' , tree ) ?. route . id ) . toBe ( '/foo/$' )
251+ } )
248252 it ( 'static+optional+static vs static+static+wildcard' , ( ) => {
249253 const tree = makeTree ( [ '/a/{-$b}/c' , '/a/b/$' ] )
250254 expect ( findRouteMatch ( '/a/b/c' , tree ) ?. route . id ) . toBe ( '/a/b/$' )
251255 } )
256+ it ( 'static+optional+static vs static+static+dynamic' , ( ) => {
257+ const tree = makeTree ( [ '/a/{-$b}/c/d' , '/a/b/c/$d' ] )
258+ expect ( findRouteMatch ( '/a/b/c/d' , tree ) ?. route . id ) . toBe ( '/a/b/c/$d' )
259+ } )
260+ it ( 'static+optional+static+static beats static+dynamic+static+optional' , ( ) => {
261+ const tree = makeTree ( [ '/a/$b/c/{-$d}' , '/a/{-$b}/c/d' ] )
262+ expect ( findRouteMatch ( '/a/b/c/d' , tree ) ?. route . id ) . toBe ( '/a/{-$b}/c/d' )
263+ } )
252264 it ( 'static+dynamic+wildcard vs static+optional+static' , ( ) => {
253265 const tree = makeTree ( [ '/a/$b/$' , '/a/{-$b}/c' ] )
254266 expect ( findRouteMatch ( '/a/b/c' , tree ) ?. route . id ) . toBe ( '/a/{-$b}/c' )
@@ -267,6 +279,10 @@ describe('findRouteMatch', () => {
267279 const tree = makeTree ( [ '/{-$a}/b/c' , '/b/$' ] )
268280 expect ( findRouteMatch ( '/b/c' , tree ) ?. route . id ) . toBe ( '/{-$a}/b/c' )
269281 } )
282+ it ( 'skipped optional aligns later static priority with wildcard siblings' , ( ) => {
283+ const tree = makeTree ( [ '/{-$a}/b/{-$c}/d' , '/b/c/$' ] )
284+ expect ( findRouteMatch ( '/b/c/d' , tree ) ?. route . id ) . toBe ( '/b/c/$' )
285+ } )
270286 it ( 'optional child wins over empty wildcard at the same node' , ( ) => {
271287 const tree = makeTree ( [ '/a/{-$b}' , '/a/$' ] )
272288 expect ( findRouteMatch ( '/a' , tree ) ?. route . id ) . toBe ( '/a/{-$b}' )
@@ -825,6 +841,72 @@ describe('findRouteMatch', () => {
825841 expect ( match ?. route . id ) . toBe ( '/a/$' )
826842 expect ( match ?. rawParams ) . toEqual ( { '*' : 'b/c' , _splat : 'b/c' } )
827843 } )
844+
845+ it ( 'falls back to fuzzy layout matching when wildcard param parsing fails' , ( ) => {
846+ const tree = {
847+ id : '__root__' ,
848+ fullPath : '/' ,
849+ path : '/' ,
850+ isRoot : true ,
851+ options : { } ,
852+ children : [
853+ {
854+ id : '/a' ,
855+ fullPath : '/a' ,
856+ path : 'a' ,
857+ } ,
858+ {
859+ id : '/a/$' ,
860+ fullPath : '/a/$' ,
861+ path : 'a/$' ,
862+ options : {
863+ params : {
864+ parse : ( ) => {
865+ throw new Error ( 'skip' )
866+ } ,
867+ } ,
868+ skipRouteOnParseError : { params : true } ,
869+ } ,
870+ } ,
871+ ] ,
872+ }
873+ const { processedTree } = processRouteTree ( tree )
874+ const match = findRouteMatch ( '/a/b/c' , processedTree , true )
875+ expect ( match ?. route . id ) . toBe ( '/a' )
876+ expect ( match ?. rawParams ) . toEqual ( { '**' : 'b/c' } )
877+ } )
878+
879+ it ( 'falls back to exact layout matching when empty wildcard param parsing fails' , ( ) => {
880+ const tree = {
881+ id : '__root__' ,
882+ fullPath : '/' ,
883+ path : '/' ,
884+ isRoot : true ,
885+ options : { } ,
886+ children : [
887+ {
888+ id : '/a' ,
889+ fullPath : '/a' ,
890+ path : 'a' ,
891+ } ,
892+ {
893+ id : '/a/$' ,
894+ fullPath : '/a/$' ,
895+ path : 'a/$' ,
896+ options : {
897+ params : {
898+ parse : ( ) => {
899+ throw new Error ( 'skip' )
900+ } ,
901+ } ,
902+ skipRouteOnParseError : { params : true } ,
903+ } ,
904+ } ,
905+ ] ,
906+ }
907+ const { processedTree } = processRouteTree ( tree )
908+ expect ( findRouteMatch ( '/a' , processedTree ) ?. route . id ) . toBe ( '/a' )
909+ } )
828910 } )
829911
830912 describe ( 'param extraction' , ( ) => {
@@ -1003,6 +1085,49 @@ describe('findRouteMatch', () => {
10031085 expect ( result ?. route . id ) . toBe ( '/a/$' )
10041086 expect ( result ?. rawParams ) . toEqual ( { '*' : 'foobar' , _splat : 'foobar' } )
10051087 } )
1088+ it ( 'falls back to the next matching wildcard by priority when the best wildcard parse fails' , ( ) => {
1089+ const tree = {
1090+ id : '__root__' ,
1091+ fullPath : '/' ,
1092+ path : '/' ,
1093+ isRoot : true ,
1094+ options : { } ,
1095+ children : [
1096+ {
1097+ id : '/a/foo{$}bar' ,
1098+ fullPath : '/a/foo{$}bar' ,
1099+ path : 'a/foo{$}bar' ,
1100+ isRoot : false ,
1101+ options : {
1102+ params : {
1103+ parse : ( ) => {
1104+ throw new Error ( 'skip' )
1105+ } ,
1106+ } ,
1107+ skipRouteOnParseError : { params : true } ,
1108+ } ,
1109+ } ,
1110+ {
1111+ id : '/a/foo{$}' ,
1112+ fullPath : '/a/foo{$}' ,
1113+ path : 'a/foo{$}' ,
1114+ isRoot : false ,
1115+ options : { } ,
1116+ } ,
1117+ {
1118+ id : '/a/$' ,
1119+ fullPath : '/a/$' ,
1120+ path : 'a/$' ,
1121+ isRoot : false ,
1122+ options : { } ,
1123+ } ,
1124+ ] ,
1125+ }
1126+ const { processedTree } = processRouteTree ( tree )
1127+ const result = findRouteMatch ( '/a/fooxbar' , processedTree )
1128+ expect ( result ?. route . id ) . toBe ( '/a/foo{$}' )
1129+ expect ( result ?. rawParams ) . toEqual ( { '*' : 'xbar' , _splat : 'xbar' } )
1130+ } )
10061131 it ( 'does not validate a lower-priority wildcard after a better wildcard matches' , ( ) => {
10071132 let higherPriorityCalls = 0
10081133 let lowerPriorityCalls = 0
@@ -1052,6 +1177,82 @@ describe('findRouteMatch', () => {
10521177 expect ( higherPriorityCalls ) . toBe ( 1 )
10531178 expect ( lowerPriorityCalls ) . toBe ( 0 )
10541179 } )
1180+ it ( 'does not validate wildcard fallback after a static sibling matches' , ( ) => {
1181+ let calls = 0
1182+ const tree = {
1183+ id : '__root__' ,
1184+ fullPath : '/' ,
1185+ path : '/' ,
1186+ isRoot : true ,
1187+ options : { } ,
1188+ children : [
1189+ {
1190+ id : '/a/b' ,
1191+ fullPath : '/a/b' ,
1192+ path : 'a/b' ,
1193+ isRoot : false ,
1194+ options : { } ,
1195+ } ,
1196+ {
1197+ id : '/a/$' ,
1198+ fullPath : '/a/$' ,
1199+ path : 'a/$' ,
1200+ isRoot : false ,
1201+ options : {
1202+ params : {
1203+ parse : ( params : Record < string , string > ) => {
1204+ calls ++
1205+ return params
1206+ } ,
1207+ } ,
1208+ skipRouteOnParseError : { params : true } ,
1209+ } ,
1210+ } ,
1211+ ] ,
1212+ }
1213+ const { processedTree } = processRouteTree ( tree )
1214+ const result = findRouteMatch ( '/a/b' , processedTree )
1215+ expect ( result ?. route . id ) . toBe ( '/a/b' )
1216+ expect ( calls ) . toBe ( 0 )
1217+ } )
1218+ it ( 'does not validate empty wildcard fallback after an index sibling matches' , ( ) => {
1219+ let calls = 0
1220+ const tree = {
1221+ id : '__root__' ,
1222+ fullPath : '/' ,
1223+ path : '/' ,
1224+ isRoot : true ,
1225+ options : { } ,
1226+ children : [
1227+ {
1228+ id : '/a/' ,
1229+ fullPath : '/a/' ,
1230+ path : 'a/' ,
1231+ isRoot : false ,
1232+ options : { } ,
1233+ } ,
1234+ {
1235+ id : '/a/$' ,
1236+ fullPath : '/a/$' ,
1237+ path : 'a/$' ,
1238+ isRoot : false ,
1239+ options : {
1240+ params : {
1241+ parse : ( params : Record < string , string > ) => {
1242+ calls ++
1243+ return params
1244+ } ,
1245+ } ,
1246+ skipRouteOnParseError : { params : true } ,
1247+ } ,
1248+ } ,
1249+ ] ,
1250+ }
1251+ const { processedTree } = processRouteTree ( tree )
1252+ const result = findRouteMatch ( '/a' , processedTree )
1253+ expect ( result ?. route . id ) . toBe ( '/a/' )
1254+ expect ( calls ) . toBe ( 0 )
1255+ } )
10551256 it ( 'preserves parsed params from a stacked wildcard route' , ( ) => {
10561257 const tree = {
10571258 id : '__root__' ,
@@ -1085,6 +1286,36 @@ describe('findRouteMatch', () => {
10851286 } )
10861287 } )
10871288 describe ( 'pathless routes' , ( ) => {
1289+ it ( 'pathless nodes do not shift positional priority scores' , ( ) => {
1290+ const tree = {
1291+ id : '__root__' ,
1292+ fullPath : '/' ,
1293+ path : '/' ,
1294+ isRoot : true ,
1295+ children : [
1296+ {
1297+ id : '/_layout' ,
1298+ fullPath : '/' ,
1299+ children : [
1300+ {
1301+ id : '/_layout/a/b' ,
1302+ fullPath : '/a/b' ,
1303+ path : 'a/b' ,
1304+ } ,
1305+ ] ,
1306+ } ,
1307+ {
1308+ id : '/a/$' ,
1309+ fullPath : '/a/$' ,
1310+ path : 'a/$' ,
1311+ } ,
1312+ ] ,
1313+ }
1314+ const { processedTree } = processRouteTree ( tree )
1315+ expect ( findRouteMatch ( '/a/b' , processedTree ) ?. route . id ) . toBe (
1316+ '/_layout/a/b' ,
1317+ )
1318+ } )
10881319 it ( 'builds segment tree correctly' , ( ) => {
10891320 const tree = {
10901321 path : '/' ,
0 commit comments