]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
feat(experimental): handle redirect types
authorEduardo San Martin Morote <posva13@gmail.com>
Sat, 18 Oct 2025 05:56:19 +0000 (07:56 +0200)
committerEduardo San Martin Morote <posva13@gmail.com>
Sat, 18 Oct 2025 05:56:19 +0000 (07:56 +0200)
packages/router/src/experimental/router.spec.ts
packages/router/src/experimental/router.ts

index 2afe5c93442c871c050f57bd0fcad34ef5caf01d..116a8fb036e299ae351eb35aa2115fa6e8de4e35 100644 (file)
@@ -104,6 +104,23 @@ const childRawRecord: EXPERIMENTAL_RouteRecord_Matchable = {
   parent: parentRecord,
 }
 
+const parentWithRedirectRawRecord: EXPERIMENTAL_RouteRecord_Matchable = {
+  name: 'parent-with-redirect',
+  path: new MatcherPatternPathStatic('/parent-with-redirect'),
+  redirect: { name: 'child-for-redirect' },
+}
+const parentWithRedirectRecord = normalizeRouteRecord(
+  parentWithRedirectRawRecord
+)
+
+const childDefaultRawRecord: EXPERIMENTAL_RouteRecord_Matchable = {
+  name: 'child-for-redirect',
+  path: new MatcherPatternPathStatic('/parent-with-redirect'),
+  components: { default: components.Foo },
+  meta: { fromParent: 'foo' },
+  parent: parentWithRedirectRecord,
+}
+
 // Create all route records
 const routeRecords: EXPERIMENTAL_RouteRecord_Matchable[] = [
   {
@@ -114,8 +131,6 @@ const routeRecords: EXPERIMENTAL_RouteRecord_Matchable[] = [
   {
     name: 'home-redirect',
     path: new MatcherPatternPathStatic('/home'),
-    // TODO: this should not be needed in a redirect record
-    components: { default: components.Home },
     redirect: { name: 'home' },
   },
   {
@@ -137,8 +152,6 @@ const routeRecords: EXPERIMENTAL_RouteRecord_Matchable[] = [
   {
     name: Symbol('to-foo'),
     path: new MatcherPatternPathStatic('/to-foo'),
-    // TODO: this should not be needed in a redirect record
-    components: { default: components.Home },
     redirect: to => ({
       path: '/foo',
       query: to.query,
@@ -148,15 +161,11 @@ const routeRecords: EXPERIMENTAL_RouteRecord_Matchable[] = [
   {
     name: Symbol('to-foo2'),
     path: new MatcherPatternPathStatic('/to-foo2'),
-    // TODO: this should not be needed in a redirect record
-    components: { default: components.Home },
     redirect: '/to-foo',
   },
   {
     path: new MatcherPatternPathStatic('/to-foo-query'),
     name: Symbol('to-foo-query'),
-    // TODO: this should not be needed in a redirect record
-    components: { default: components.Home },
     redirect: '/foo?a=2#b',
   },
 
@@ -166,8 +175,6 @@ const routeRecords: EXPERIMENTAL_RouteRecord_Matchable[] = [
       'to-p',
       1,
     ]),
-    // TODO: this should not be needed in a redirect record
-    components: { default: components.Home },
     redirect: to => ({
       name: 'Param',
       params: to.params,
@@ -201,8 +208,12 @@ const routeRecords: EXPERIMENTAL_RouteRecord_Matchable[] = [
     path: new MatcherPatternPathStatic('/before-leave'),
     components: { default: components.BeforeLeave },
   },
-  parentRawRecord,
+
   childRawRecord,
+  parentRawRecord,
+
+  childDefaultRawRecord,
+  parentWithRedirectRecord,
 
   {
     name: 'param-with-slashes',
@@ -234,10 +245,18 @@ const routeRecords: EXPERIMENTAL_RouteRecord_Matchable[] = [
       { p: [] },
       ['redirect-with-param', 1]
     ),
-    // TODO: shouldn't be needed in a redirect record
-    components: { default: components.Foo },
     redirect: () => `/`,
   },
+  {
+    name: Symbol('inc-query-hash'),
+    // path: '/inc-query-hash',
+    path: new MatcherPatternPathStatic('/inc-query-hash'),
+    redirect: to => ({
+      name: 'Foo',
+      query: { n: to.query.n + '-2' },
+      hash: to.hash + '-2',
+    }),
+  },
 
   {
     name: 'catch-all',
@@ -991,16 +1010,46 @@ describe('Experimental Router', () => {
         redirectedFrom: expect.objectContaining({ path: '/to-foo' }),
       })
 
+      const navPromise = nextNavigation(router as any)
       history.go(-1)
-      await nextNavigation(router as any)
+      await navPromise
       expect(router.currentRoute.value).not.toMatchObject({
         path: '/search',
       })
     })
 
-    it.skip('can pass on query and hash when redirecting', async () => {})
+    it('can pass on query and hash when redirecting', async () => {
+      const { router } = await newRouter()
+      await router.push('/inc-query-hash?n=3#fa')
+      const loc = router.currentRoute.value
+      expect(loc).toMatchObject({
+        name: 'Foo',
+        query: {
+          n: '3-2',
+        },
+        hash: '#fa-2',
+      })
+      expect(loc.redirectedFrom).toMatchObject({
+        fullPath: '/inc-query-hash?n=3#fa',
+        query: { n: '3' },
+        hash: '#fa',
+        path: '/inc-query-hash',
+      })
+    })
 
-    it.skip('allows a redirect with children', async () => {})
+    it('allows a redirect with children', async () => {
+      const { router } = await newRouter()
+      await expect(
+        router.push({ name: 'parent-with-redirect' })
+      ).resolves.toEqual(undefined)
+      const loc = router.currentRoute.value
+      expect(loc.path).toBe('/parent-with-redirect')
+      expect(loc.name).toBe('child-for-redirect')
+      expect(loc.redirectedFrom).toMatchObject({
+        name: 'parent-with-redirect',
+        path: '/parent-with-redirect',
+      })
+    })
 
     it.skip('works with named routes', async () => {})
   })
index 9f566a6828392251a2796d7e3b37cfb1b3319acc..dfa617bd5cb284f860f5f98b324057415a8e417a 100644 (file)
@@ -234,11 +234,13 @@ export interface EXPERIMENTAL_RouteRecord_Base
   // props?: _RouteRecordProps | Record<string, _RouteRecordProps>
 }
 
-export interface EXPERIMENTAL_RouteRecord_Matchable
+export interface EXPERIMENTAL_RouteRecord_Redirect
   // preserve the values from the type EXPERIMENTAL_ResolverRecord_Matchable
   extends Omit<EXPERIMENTAL_RouteRecord_Base, 'name' | 'path' | 'parent'>,
     EXPERIMENTAL_ResolverRecord_Matchable {
-  components: Record<string, RawRouteComponent>
+  components?: Record<string, RawRouteComponent>
+
+  redirect: RouteRecordRedirectOption // must be defined
 
   parent?: EXPERIMENTAL_RouteRecordNormalized | null
 }
@@ -255,6 +257,21 @@ export interface EXPERIMENTAL_RouteRecord_Group
   parent?: EXPERIMENTAL_RouteRecordNormalized | null
 }
 
+export interface EXPERIMENTAL_RouteRecord_Components
+  // preserve the values from the type EXPERIMENTAL_ResolverRecord_Matchable
+  extends Omit<EXPERIMENTAL_RouteRecord_Base, 'name' | 'path' | 'parent'>,
+    EXPERIMENTAL_ResolverRecord_Matchable {
+  components: Record<string, RawRouteComponent>
+
+  redirect?: never
+
+  parent?: EXPERIMENTAL_RouteRecordNormalized | null
+}
+
+export type EXPERIMENTAL_RouteRecord_Matchable =
+  | EXPERIMENTAL_RouteRecord_Components
+  | EXPERIMENTAL_RouteRecord_Redirect
+
 export type EXPERIMENTAL_RouteRecordRaw =
   | EXPERIMENTAL_RouteRecord_Matchable
   | EXPERIMENTAL_RouteRecord_Group
@@ -299,17 +316,24 @@ export interface EXPERIMENTAL_RouteRecordNormalized_Group
   parent: EXPERIMENTAL_RouteRecordNormalized | null
 }
 
-// TODO: is it worth to have 2 types for the undefined values?
-export interface EXPERIMENTAL_RouteRecordNormalized_Matchable
+export interface EXPERIMENTAL_RouteRecordNormalized_Redirect
   extends EXPERIMENTAL_RouteRecordNormalized_Base,
-    EXPERIMENTAL_RouteRecord_Matchable {
+    EXPERIMENTAL_RouteRecord_Redirect {
   meta: RouteMeta
-
   parent: EXPERIMENTAL_RouteRecordNormalized | null
+}
 
-  components: Record<string, RawRouteComponent>
+export interface EXPERIMENTAL_RouteRecordNormalized_Components
+  extends EXPERIMENTAL_RouteRecordNormalized_Base,
+    EXPERIMENTAL_RouteRecord_Components {
+  meta: RouteMeta
+  parent: EXPERIMENTAL_RouteRecordNormalized | null
 }
 
+export type EXPERIMENTAL_RouteRecordNormalized_Matchable =
+  | EXPERIMENTAL_RouteRecordNormalized_Components
+  | EXPERIMENTAL_RouteRecordNormalized_Redirect
+
 export type EXPERIMENTAL_RouteRecordNormalized =
   | EXPERIMENTAL_RouteRecordNormalized_Matchable
   | EXPERIMENTAL_RouteRecordNormalized_Group