@pyreon/router 0.7.12 → 0.7.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/router",
3
- "version": "0.7.12",
3
+ "version": "0.7.13",
4
4
  "description": "Official router for Pyreon",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -39,9 +39,9 @@
39
39
  "prepublishOnly": "bun run build"
40
40
  },
41
41
  "dependencies": {
42
- "@pyreon/core": "^0.7.12",
43
- "@pyreon/reactivity": "^0.7.12",
44
- "@pyreon/runtime-dom": "^0.7.12"
42
+ "@pyreon/core": "^0.7.13",
43
+ "@pyreon/reactivity": "^0.7.13",
44
+ "@pyreon/runtime-dom": "^0.7.13"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@happy-dom/global-registrator": "^20.8.3",
@@ -3919,3 +3919,108 @@ describe("onBeforeRouteUpdate", () => {
3919
3919
  router.destroy()
3920
3920
  })
3921
3921
  })
3922
+
3923
+ // ─── beforeEach guard edge cases ──────────────────────────────────────────────
3924
+
3925
+ describe("beforeEach guard edge cases", () => {
3926
+ test("returning false prevents navigation and doesn't mutate state", async () => {
3927
+ const router = createRouter({ routes, url: "/" })
3928
+ const guardCalls: Array<{ to: string; from: string }> = []
3929
+ router.beforeEach((to, from) => {
3930
+ guardCalls.push({ to: to.path, from: from.path })
3931
+ return false
3932
+ })
3933
+ const initialRoute = router.currentRoute()
3934
+ await router.push("/about")
3935
+ // Route should not have changed
3936
+ expect(router.currentRoute().path).toBe("/")
3937
+ expect(router.currentRoute()).toBe(initialRoute)
3938
+ // Guard was called with correct args
3939
+ expect(guardCalls).toEqual([{ to: "/about", from: "/" }])
3940
+ })
3941
+
3942
+ test("throwing error doesn't break router state", async () => {
3943
+ const router = createRouter({ routes, url: "/" })
3944
+ router.beforeEach(() => {
3945
+ throw new Error("guard explosion")
3946
+ })
3947
+ await router.push("/about")
3948
+ // Navigation should be blocked but router still functional
3949
+ expect(router.currentRoute().path).toBe("/")
3950
+ // Subsequent navigation with a non-throwing guard should work
3951
+ // Remove the throwing guard by adding a new one that allows
3952
+ // (guards run in order; the throwing one still runs first, so replace via fresh router)
3953
+ })
3954
+
3955
+ test("throwing error doesn't prevent subsequent navigations", async () => {
3956
+ const router = createRouter({ routes, url: "/" })
3957
+ let shouldThrow = true
3958
+ router.beforeEach(() => {
3959
+ if (shouldThrow) throw new Error("guard error")
3960
+ return true
3961
+ })
3962
+ await router.push("/about")
3963
+ expect(router.currentRoute().path).toBe("/")
3964
+
3965
+ // Disable throwing and navigate again — router should still work
3966
+ shouldThrow = false
3967
+ await router.push("/about")
3968
+ expect(router.currentRoute().path).toBe("/about")
3969
+ })
3970
+
3971
+ test("navigation to same route keeps state stable", async () => {
3972
+ const router = createRouter({ routes, url: "/about" })
3973
+ const guardCalls: string[] = []
3974
+ router.beforeEach((to) => {
3975
+ guardCalls.push(to.path)
3976
+ return true
3977
+ })
3978
+ await router.push("/about")
3979
+ // State should remain at /about regardless of whether guards ran
3980
+ expect(router.currentRoute().path).toBe("/about")
3981
+ // Guard was invoked (router does not short-circuit same-route navigation)
3982
+ expect(guardCalls).toEqual(["/about"])
3983
+ })
3984
+ })
3985
+
3986
+ // ─── Optional params & trailing slash ─────────────────────────────────────────
3987
+
3988
+ describe("matchPath — optional params", () => {
3989
+ test("optional param matches path without the param", () => {
3990
+ const result = matchPath("/users/:id?", "/users")
3991
+ expect(result).not.toBeNull()
3992
+ expect(result).toEqual({})
3993
+ })
3994
+
3995
+ test("optional param matches path with the param", () => {
3996
+ const result = matchPath("/users/:id?", "/users/42")
3997
+ expect(result).not.toBeNull()
3998
+ expect(result).toEqual({ id: "42" })
3999
+ })
4000
+
4001
+ test("optional param at end doesn't match extra segments", () => {
4002
+ const result = matchPath("/users/:id?", "/users/42/extra")
4003
+ expect(result).toBeNull()
4004
+ })
4005
+ })
4006
+
4007
+ describe("resolveRoute — trailing slash normalization", () => {
4008
+ test("trailing slash on static path resolves the same route", () => {
4009
+ // resolveRoute uses split("/").filter(Boolean), so "/about/" and "/about" produce same segments
4010
+ const withSlash = resolveRoute("/about/", routes)
4011
+ const withoutSlash = resolveRoute("/about", routes)
4012
+ expect(withSlash.matched.length).toBe(withoutSlash.matched.length)
4013
+ if (withSlash.matched.length > 0 && withoutSlash.matched.length > 0) {
4014
+ expect(withSlash.matched[withSlash.matched.length - 1]?.component).toBe(
4015
+ withoutSlash.matched[withoutSlash.matched.length - 1]?.component,
4016
+ )
4017
+ }
4018
+ })
4019
+
4020
+ test("trailing slash on dynamic path resolves the same route", () => {
4021
+ const withSlash = resolveRoute("/user/42/", routes)
4022
+ const withoutSlash = resolveRoute("/user/42", routes)
4023
+ expect(withSlash.params).toEqual(withoutSlash.params)
4024
+ expect(withSlash.matched.length).toBe(withoutSlash.matched.length)
4025
+ })
4026
+ })