Skip to content

Commit bc9fba1

Browse files
committed
tests: add tests
1 parent 70a7227 commit bc9fba1

File tree

6 files changed

+266
-0
lines changed

6 files changed

+266
-0
lines changed

packages/nextjs/test/client/parameterization.test.ts

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,4 +644,261 @@ describe('maybeParameterizeRoute', () => {
644644
expect(maybeParameterizeRoute('/some/random/path')).toBe('/:catchall*');
645645
});
646646
});
647+
648+
describe('i18n routing with optional prefix', () => {
649+
it('should match routes with optional locale prefix for default locale paths', () => {
650+
const manifest: RouteManifest = {
651+
staticRoutes: [{ path: '/' }],
652+
dynamicRoutes: [
653+
{
654+
path: '/:locale',
655+
regex: '^/([^/]+)$',
656+
paramNames: ['locale'],
657+
hasOptionalPrefix: true,
658+
},
659+
{
660+
path: '/:locale/foo',
661+
regex: '^/([^/]+)/foo$',
662+
paramNames: ['locale'],
663+
hasOptionalPrefix: true,
664+
},
665+
{
666+
path: '/:locale/bar',
667+
regex: '^/([^/]+)/bar$',
668+
paramNames: ['locale'],
669+
hasOptionalPrefix: true,
670+
},
671+
{
672+
path: '/:locale/products',
673+
regex: '^/([^/]+)/products$',
674+
paramNames: ['locale'],
675+
hasOptionalPrefix: true,
676+
},
677+
],
678+
};
679+
globalWithInjectedManifest._sentryRouteManifest = JSON.stringify(manifest);
680+
681+
// Default locale paths (without prefix) should match parameterized routes
682+
expect(maybeParameterizeRoute('/foo')).toBe('/:locale/foo');
683+
expect(maybeParameterizeRoute('/bar')).toBe('/:locale/bar');
684+
expect(maybeParameterizeRoute('/products')).toBe('/:locale/products');
685+
686+
// Non-default locale paths (with prefix) should also match
687+
expect(maybeParameterizeRoute('/ar/foo')).toBe('/:locale/foo');
688+
expect(maybeParameterizeRoute('/ar/bar')).toBe('/:locale/bar');
689+
expect(maybeParameterizeRoute('/ar/products')).toBe('/:locale/products');
690+
expect(maybeParameterizeRoute('/en/foo')).toBe('/:locale/foo');
691+
expect(maybeParameterizeRoute('/fr/products')).toBe('/:locale/products');
692+
});
693+
694+
it('should handle nested routes with optional locale prefix', () => {
695+
const manifest: RouteManifest = {
696+
staticRoutes: [],
697+
dynamicRoutes: [
698+
{
699+
path: '/:locale/foo/:id',
700+
regex: '^/([^/]+)/foo/([^/]+)$',
701+
paramNames: ['locale', 'id'],
702+
hasOptionalPrefix: true,
703+
},
704+
{
705+
path: '/:locale/products/:productId',
706+
regex: '^/([^/]+)/products/([^/]+)$',
707+
paramNames: ['locale', 'productId'],
708+
hasOptionalPrefix: true,
709+
},
710+
],
711+
};
712+
globalWithInjectedManifest._sentryRouteManifest = JSON.stringify(manifest);
713+
714+
// Default locale (no prefix)
715+
expect(maybeParameterizeRoute('/foo/123')).toBe('/:locale/foo/:id');
716+
expect(maybeParameterizeRoute('/products/abc')).toBe('/:locale/products/:productId');
717+
718+
// Non-default locale (with prefix)
719+
expect(maybeParameterizeRoute('/ar/foo/123')).toBe('/:locale/foo/:id');
720+
expect(maybeParameterizeRoute('/ar/products/abc')).toBe('/:locale/products/:productId');
721+
expect(maybeParameterizeRoute('/en/foo/456')).toBe('/:locale/foo/:id');
722+
});
723+
724+
it('should prioritize direct matches over optional prefix matches', () => {
725+
const manifest: RouteManifest = {
726+
staticRoutes: [],
727+
dynamicRoutes: [
728+
{
729+
path: '/foo/:id',
730+
regex: '^/foo/([^/]+)$',
731+
paramNames: ['id'],
732+
},
733+
{
734+
path: '/:locale/foo',
735+
regex: '^/([^/]+)/foo$',
736+
paramNames: ['locale'],
737+
hasOptionalPrefix: true,
738+
},
739+
],
740+
};
741+
globalWithInjectedManifest._sentryRouteManifest = JSON.stringify(manifest);
742+
743+
// Direct match should win
744+
expect(maybeParameterizeRoute('/foo/123')).toBe('/foo/:id');
745+
746+
// Optional prefix match when direct match isn't available
747+
expect(maybeParameterizeRoute('/foo')).toBe('/:locale/foo');
748+
expect(maybeParameterizeRoute('/ar/foo')).toBe('/:locale/foo');
749+
});
750+
751+
it('should handle lang and language parameters as optional prefixes', () => {
752+
const manifestWithLang: RouteManifest = {
753+
staticRoutes: [],
754+
dynamicRoutes: [
755+
{
756+
path: '/:lang/page',
757+
regex: '^/([^/]+)/page$',
758+
paramNames: ['lang'],
759+
hasOptionalPrefix: true,
760+
},
761+
],
762+
};
763+
globalWithInjectedManifest._sentryRouteManifest = JSON.stringify(manifestWithLang);
764+
expect(maybeParameterizeRoute('/page')).toBe('/:lang/page');
765+
expect(maybeParameterizeRoute('/en/page')).toBe('/:lang/page');
766+
767+
const manifestWithLanguage: RouteManifest = {
768+
staticRoutes: [],
769+
dynamicRoutes: [
770+
{
771+
path: '/:language/page',
772+
regex: '^/([^/]+)/page$',
773+
paramNames: ['language'],
774+
hasOptionalPrefix: true,
775+
},
776+
],
777+
};
778+
globalWithInjectedManifest._sentryRouteManifest = JSON.stringify(manifestWithLanguage);
779+
expect(maybeParameterizeRoute('/page')).toBe('/:language/page');
780+
expect(maybeParameterizeRoute('/en/page')).toBe('/:language/page');
781+
});
782+
783+
it('should not apply optional prefix logic to non-i18n dynamic segments', () => {
784+
const manifest: RouteManifest = {
785+
staticRoutes: [],
786+
dynamicRoutes: [
787+
{
788+
path: '/:userId/profile',
789+
regex: '^/([^/]+)/profile$',
790+
paramNames: ['userId'],
791+
hasOptionalPrefix: false,
792+
},
793+
],
794+
};
795+
globalWithInjectedManifest._sentryRouteManifest = JSON.stringify(manifest);
796+
797+
// Should not match without the userId segment
798+
expect(maybeParameterizeRoute('/profile')).toBeUndefined();
799+
800+
// Should match with the userId segment
801+
expect(maybeParameterizeRoute('/123/profile')).toBe('/:userId/profile');
802+
});
803+
804+
it('should handle real-world next-intl scenario', () => {
805+
const manifest: RouteManifest = {
806+
staticRoutes: [{ path: '/' }],
807+
dynamicRoutes: [
808+
{
809+
path: '/:locale',
810+
regex: '^/([^/]+)$',
811+
paramNames: ['locale'],
812+
hasOptionalPrefix: true,
813+
},
814+
{
815+
path: '/:locale/hola',
816+
regex: '^/([^/]+)/hola$',
817+
paramNames: ['locale'],
818+
hasOptionalPrefix: true,
819+
},
820+
{
821+
path: '/:locale/products',
822+
regex: '^/([^/]+)/products$',
823+
paramNames: ['locale'],
824+
hasOptionalPrefix: true,
825+
},
826+
],
827+
};
828+
globalWithInjectedManifest._sentryRouteManifest = JSON.stringify(manifest);
829+
830+
// Root should not be parameterized (it's a static route)
831+
expect(maybeParameterizeRoute('/')).toBeUndefined();
832+
833+
// Default locale (English, no prefix) - this was the bug
834+
expect(maybeParameterizeRoute('/hola')).toBe('/:locale/hola');
835+
expect(maybeParameterizeRoute('/products')).toBe('/:locale/products');
836+
837+
// Non-default locale (Arabic, with prefix)
838+
expect(maybeParameterizeRoute('/ar')).toBe('/:locale');
839+
expect(maybeParameterizeRoute('/ar/hola')).toBe('/:locale/hola');
840+
expect(maybeParameterizeRoute('/ar/products')).toBe('/:locale/products');
841+
842+
// Other locales
843+
expect(maybeParameterizeRoute('/en/hola')).toBe('/:locale/hola');
844+
expect(maybeParameterizeRoute('/fr/products')).toBe('/:locale/products');
845+
});
846+
847+
it('should prefer more specific routes over optional prefix matches', () => {
848+
const manifest: RouteManifest = {
849+
staticRoutes: [],
850+
dynamicRoutes: [
851+
{
852+
path: '/:locale',
853+
regex: '^/([^/]+)$',
854+
paramNames: ['locale'],
855+
hasOptionalPrefix: true,
856+
},
857+
{
858+
path: '/:locale/foo/:id',
859+
regex: '^/([^/]+)/foo/([^/]+)$',
860+
paramNames: ['locale', 'id'],
861+
hasOptionalPrefix: true,
862+
},
863+
{
864+
path: '/:locale/foo',
865+
regex: '^/([^/]+)/foo$',
866+
paramNames: ['locale'],
867+
hasOptionalPrefix: true,
868+
},
869+
],
870+
};
871+
globalWithInjectedManifest._sentryRouteManifest = JSON.stringify(manifest);
872+
873+
// More specific route should win (specificity score)
874+
expect(maybeParameterizeRoute('/foo/123')).toBe('/:locale/foo/:id');
875+
expect(maybeParameterizeRoute('/foo')).toBe('/:locale/foo');
876+
expect(maybeParameterizeRoute('/about')).toBe('/:locale');
877+
});
878+
879+
it('should handle deeply nested i18n routes', () => {
880+
const manifest: RouteManifest = {
881+
staticRoutes: [],
882+
dynamicRoutes: [
883+
{
884+
path: '/:locale/users/:userId/posts/:postId/comments/:commentId',
885+
regex: '^/([^/]+)/users/([^/]+)/posts/([^/]+)/comments/([^/]+)$',
886+
paramNames: ['locale', 'userId', 'postId', 'commentId'],
887+
hasOptionalPrefix: true,
888+
},
889+
],
890+
};
891+
globalWithInjectedManifest._sentryRouteManifest = JSON.stringify(manifest);
892+
893+
// Without locale prefix (default locale)
894+
expect(maybeParameterizeRoute('/users/123/posts/456/comments/789')).toBe(
895+
'/:locale/users/:userId/posts/:postId/comments/:commentId',
896+
);
897+
898+
// With locale prefix
899+
expect(maybeParameterizeRoute('/ar/users/123/posts/456/comments/789')).toBe(
900+
'/:locale/users/:userId/posts/:postId/comments/:commentId',
901+
);
902+
});
903+
});
647904
});

packages/nextjs/test/config/manifest/suites/base-path/base-path.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ describe('basePath', () => {
1616
path: '/my-app/users/:id',
1717
regex: '^/my-app/users/([^/]+)$',
1818
paramNames: ['id'],
19+
hasOptionalPrefix: false,
1920
},
2021
],
2122
});

packages/nextjs/test/config/manifest/suites/catchall-at-root/catchall-at-root.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ describe('catchall', () => {
1313
path: '/:path*?',
1414
regex: '^/(.*)$',
1515
paramNames: ['path'],
16+
hasOptionalPrefix: false,
1617
},
1718
],
1819
});

packages/nextjs/test/config/manifest/suites/catchall/catchall.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ describe('catchall', () => {
1313
path: '/catchall/:path*?',
1414
regex: '^/catchall(?:/(.*))?$',
1515
paramNames: ['path'],
16+
hasOptionalPrefix: false,
1617
},
1718
],
1819
});

packages/nextjs/test/config/manifest/suites/dynamic/dynamic.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,25 @@ describe('dynamic', () => {
1313
path: '/dynamic/:id',
1414
regex: '^/dynamic/([^/]+)$',
1515
paramNames: ['id'],
16+
hasOptionalPrefix: false,
1617
},
1718
{
1819
path: '/users/:id',
1920
regex: '^/users/([^/]+)$',
2021
paramNames: ['id'],
22+
hasOptionalPrefix: false,
2123
},
2224
{
2325
path: '/users/:id/posts/:postId',
2426
regex: '^/users/([^/]+)/posts/([^/]+)$',
2527
paramNames: ['id', 'postId'],
28+
hasOptionalPrefix: false,
2629
},
2730
{
2831
path: '/users/:id/settings',
2932
regex: '^/users/([^/]+)/settings$',
3033
paramNames: ['id'],
34+
hasOptionalPrefix: false,
3135
},
3236
],
3337
});

packages/nextjs/test/config/manifest/suites/route-groups/route-groups.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ describe('route-groups', () => {
2323
path: '/dashboard/:id',
2424
regex: '^/dashboard/([^/]+)$',
2525
paramNames: ['id'],
26+
hasOptionalPrefix: false,
2627
},
2728
],
2829
});
@@ -55,6 +56,7 @@ describe('route-groups', () => {
5556
path: '/(dashboard)/dashboard/:id',
5657
regex: '^/\\(dashboard\\)/dashboard/([^/]+)$',
5758
paramNames: ['id'],
59+
hasOptionalPrefix: false,
5860
},
5961
],
6062
});

0 commit comments

Comments
 (0)