@planet-matrix/mobius-model 0.5.0 → 0.9.0

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.
Files changed (379) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +123 -36
  3. package/dist/index.js +715 -4
  4. package/dist/index.js.map +981 -13
  5. package/oxlint.config.ts +6 -0
  6. package/package.json +36 -18
  7. package/src/abort/README.md +92 -0
  8. package/src/abort/abort-manager.ts +278 -0
  9. package/src/abort/abort-signal-listener-manager.ts +81 -0
  10. package/src/abort/index.ts +2 -0
  11. package/src/ai/README.md +1 -0
  12. package/src/ai/ai.ts +107 -0
  13. package/src/ai/chat-completion-ai/aihubmix-chat-completion.ts +78 -0
  14. package/src/ai/chat-completion-ai/chat-completion-ai.ts +270 -0
  15. package/src/ai/chat-completion-ai/chat-completion.ts +189 -0
  16. package/src/ai/chat-completion-ai/index.ts +7 -0
  17. package/src/ai/chat-completion-ai/lingyiwanwu-chat-completion.ts +78 -0
  18. package/src/ai/chat-completion-ai/ohmygpt-chat-completion.ts +78 -0
  19. package/src/ai/chat-completion-ai/openai-next-chat-completion.ts +78 -0
  20. package/src/ai/embedding-ai/embedding-ai.ts +63 -0
  21. package/src/ai/embedding-ai/embedding.ts +50 -0
  22. package/src/ai/embedding-ai/index.ts +4 -0
  23. package/src/ai/embedding-ai/openai-next-embedding.ts +23 -0
  24. package/src/ai/index.ts +4 -0
  25. package/src/aio/README.md +100 -0
  26. package/src/aio/content.ts +141 -0
  27. package/src/aio/index.ts +3 -0
  28. package/src/aio/json.ts +127 -0
  29. package/src/aio/prompt.ts +246 -0
  30. package/src/basic/README.md +72 -116
  31. package/src/basic/error.ts +19 -5
  32. package/src/basic/function.ts +83 -64
  33. package/src/basic/index.ts +1 -0
  34. package/src/basic/is.ts +152 -71
  35. package/src/basic/promise.ts +29 -8
  36. package/src/basic/schedule.ts +111 -0
  37. package/src/basic/stream.ts +135 -25
  38. package/src/basic/string.ts +2 -33
  39. package/src/color/README.md +105 -0
  40. package/src/color/index.ts +3 -0
  41. package/src/color/internal.ts +42 -0
  42. package/src/color/rgb/analyze.ts +236 -0
  43. package/src/color/rgb/construct.ts +130 -0
  44. package/src/color/rgb/convert.ts +227 -0
  45. package/src/color/rgb/derive.ts +303 -0
  46. package/src/color/rgb/index.ts +6 -0
  47. package/src/color/rgb/internal.ts +208 -0
  48. package/src/color/rgb/parse.ts +302 -0
  49. package/src/color/rgb/serialize.ts +144 -0
  50. package/src/color/types.ts +57 -0
  51. package/src/color/xyz/analyze.ts +80 -0
  52. package/src/color/xyz/construct.ts +19 -0
  53. package/src/color/xyz/convert.ts +71 -0
  54. package/src/color/xyz/index.ts +3 -0
  55. package/src/color/xyz/internal.ts +23 -0
  56. package/src/credential/README.md +107 -0
  57. package/src/credential/api-key.ts +158 -0
  58. package/src/credential/bearer.ts +73 -0
  59. package/src/credential/index.ts +4 -0
  60. package/src/credential/json-web-token.ts +96 -0
  61. package/src/credential/password.ts +170 -0
  62. package/src/cron/README.md +86 -0
  63. package/src/cron/cron.ts +87 -0
  64. package/src/cron/index.ts +1 -0
  65. package/src/css/README.md +93 -0
  66. package/src/css/class.ts +559 -0
  67. package/src/css/index.ts +1 -0
  68. package/src/drizzle/README.md +1 -0
  69. package/src/drizzle/drizzle.ts +1 -0
  70. package/src/drizzle/helper.ts +47 -0
  71. package/src/drizzle/index.ts +5 -0
  72. package/src/drizzle/infer.ts +52 -0
  73. package/src/drizzle/kysely.ts +8 -0
  74. package/src/drizzle/pagination.ts +200 -0
  75. package/src/email/README.md +1 -0
  76. package/src/email/index.ts +1 -0
  77. package/src/email/resend.ts +25 -0
  78. package/src/encoding/README.md +66 -79
  79. package/src/encoding/base64.ts +13 -4
  80. package/src/environment/README.md +97 -0
  81. package/src/environment/basic.ts +26 -0
  82. package/src/environment/device.ts +311 -0
  83. package/src/environment/feature.ts +285 -0
  84. package/src/environment/geo.ts +337 -0
  85. package/src/environment/index.ts +7 -0
  86. package/src/environment/runtime.ts +400 -0
  87. package/src/environment/snapshot.ts +60 -0
  88. package/src/environment/variable.ts +239 -0
  89. package/src/event/README.md +90 -0
  90. package/src/event/class-event-proxy.ts +229 -0
  91. package/src/event/common.ts +29 -0
  92. package/src/event/event-manager.ts +203 -0
  93. package/src/event/index.ts +4 -0
  94. package/src/event/instance-event-proxy.ts +187 -0
  95. package/src/event/internal.ts +24 -0
  96. package/src/exception/README.md +96 -0
  97. package/src/exception/browser.ts +219 -0
  98. package/src/exception/index.ts +4 -0
  99. package/src/exception/nodejs.ts +169 -0
  100. package/src/exception/normalize.ts +106 -0
  101. package/src/exception/types.ts +99 -0
  102. package/src/form/README.md +25 -0
  103. package/src/form/index.ts +1 -0
  104. package/src/form/inputor-controller/base.ts +874 -0
  105. package/src/form/inputor-controller/boolean.ts +39 -0
  106. package/src/form/inputor-controller/file.ts +39 -0
  107. package/src/form/inputor-controller/form.ts +181 -0
  108. package/src/form/inputor-controller/helper.ts +117 -0
  109. package/src/form/inputor-controller/index.ts +17 -0
  110. package/src/form/inputor-controller/multi-select.ts +99 -0
  111. package/src/form/inputor-controller/number.ts +116 -0
  112. package/src/form/inputor-controller/select.ts +109 -0
  113. package/src/form/inputor-controller/text.ts +82 -0
  114. package/src/http/READMD.md +1 -0
  115. package/src/http/api/api-core.ts +84 -0
  116. package/src/http/api/api-handler.ts +79 -0
  117. package/src/http/api/api-host.ts +47 -0
  118. package/src/http/api/api-result.ts +56 -0
  119. package/src/http/api/api-schema.ts +154 -0
  120. package/src/http/api/api-server.ts +130 -0
  121. package/src/http/api/api-test.ts +142 -0
  122. package/src/http/api/api-type.ts +37 -0
  123. package/src/http/api/api.ts +81 -0
  124. package/src/http/api/index.ts +11 -0
  125. package/src/http/api-adapter/api-core-node-http.ts +260 -0
  126. package/src/http/api-adapter/api-host-node-http.ts +156 -0
  127. package/src/http/api-adapter/api-result-arktype.ts +297 -0
  128. package/src/http/api-adapter/api-result-zod.ts +286 -0
  129. package/src/http/api-adapter/index.ts +5 -0
  130. package/src/http/bin/gen-api-list/gen-api-list.ts +126 -0
  131. package/src/http/bin/gen-api-list/index.ts +1 -0
  132. package/src/http/bin/gen-api-test/gen-api-test.ts +136 -0
  133. package/src/http/bin/gen-api-test/index.ts +1 -0
  134. package/src/http/bin/gen-api-type/calc-code.ts +25 -0
  135. package/src/http/bin/gen-api-type/gen-api-type.ts +127 -0
  136. package/src/http/bin/gen-api-type/index.ts +2 -0
  137. package/src/http/bin/index.ts +2 -0
  138. package/src/http/index.ts +3 -0
  139. package/src/huawei/README.md +1 -0
  140. package/src/huawei/index.ts +2 -0
  141. package/src/huawei/moderation/index.ts +1 -0
  142. package/src/huawei/moderation/moderation.ts +355 -0
  143. package/src/huawei/obs/esdk-obs-nodejs.d.ts +87 -0
  144. package/src/huawei/obs/index.ts +1 -0
  145. package/src/huawei/obs/obs.ts +42 -0
  146. package/src/identifier/README.md +92 -0
  147. package/src/identifier/id.ts +119 -0
  148. package/src/identifier/index.ts +2 -0
  149. package/src/identifier/uuid.ts +187 -0
  150. package/src/index.ts +33 -1
  151. package/src/json/README.md +92 -0
  152. package/src/json/index.ts +1 -0
  153. package/src/json/repair.ts +18 -0
  154. package/src/log/README.md +79 -0
  155. package/src/log/index.ts +5 -0
  156. package/src/log/log-emitter.ts +72 -0
  157. package/src/log/log-record.ts +10 -0
  158. package/src/log/log-scheduler.ts +74 -0
  159. package/src/log/log-type.ts +8 -0
  160. package/src/log/logger.ts +554 -0
  161. package/src/openai/README.md +1 -0
  162. package/src/openai/index.ts +1 -0
  163. package/src/openai/openai.ts +510 -0
  164. package/src/orchestration/README.md +91 -0
  165. package/src/orchestration/coordination/barrier.ts +214 -0
  166. package/src/orchestration/coordination/count-down-latch.ts +215 -0
  167. package/src/orchestration/coordination/errors.ts +98 -0
  168. package/src/orchestration/coordination/index.ts +16 -0
  169. package/src/orchestration/coordination/internal/wait-constraints.ts +95 -0
  170. package/src/orchestration/coordination/internal/wait-queue.ts +109 -0
  171. package/src/orchestration/coordination/keyed-lock.ts +168 -0
  172. package/src/orchestration/coordination/mutex.ts +257 -0
  173. package/src/orchestration/coordination/permit.ts +127 -0
  174. package/src/orchestration/coordination/read-write-lock.ts +444 -0
  175. package/src/orchestration/coordination/semaphore.ts +280 -0
  176. package/src/orchestration/dispatching/dispatcher.ts +83 -0
  177. package/src/orchestration/dispatching/index.ts +2 -0
  178. package/src/orchestration/dispatching/selector/base-selector.ts +39 -0
  179. package/src/orchestration/dispatching/selector/down-count-selector.ts +119 -0
  180. package/src/orchestration/dispatching/selector/index.ts +2 -0
  181. package/src/orchestration/index.ts +3 -0
  182. package/src/orchestration/scheduling/index.ts +2 -0
  183. package/src/orchestration/scheduling/scheduler.ts +103 -0
  184. package/src/orchestration/scheduling/task.ts +32 -0
  185. package/src/random/README.md +56 -86
  186. package/src/random/base.ts +66 -0
  187. package/src/random/index.ts +5 -1
  188. package/src/random/random-boolean.ts +40 -0
  189. package/src/random/random-integer.ts +60 -0
  190. package/src/random/random-number.ts +72 -0
  191. package/src/random/random-string.ts +66 -0
  192. package/src/reactor/README.md +4 -0
  193. package/src/reactor/reactor-core/primitive.ts +9 -9
  194. package/src/reactor/reactor-core/reactive-system.ts +5 -5
  195. package/src/request/README.md +108 -0
  196. package/src/request/fetch/base.ts +108 -0
  197. package/src/request/fetch/browser.ts +285 -0
  198. package/src/request/fetch/general.ts +20 -0
  199. package/src/request/fetch/index.ts +4 -0
  200. package/src/request/fetch/nodejs.ts +285 -0
  201. package/src/request/index.ts +2 -0
  202. package/src/request/request/base.ts +250 -0
  203. package/src/request/request/general.ts +64 -0
  204. package/src/request/request/index.ts +3 -0
  205. package/src/request/request/resource.ts +68 -0
  206. package/src/result/README.md +4 -0
  207. package/src/result/controller.ts +54 -0
  208. package/src/result/either.ts +193 -0
  209. package/src/result/index.ts +2 -0
  210. package/src/route/README.md +105 -0
  211. package/src/route/adapter/browser.ts +122 -0
  212. package/src/route/adapter/driver.ts +56 -0
  213. package/src/route/adapter/index.ts +2 -0
  214. package/src/route/index.ts +3 -0
  215. package/src/route/router/index.ts +2 -0
  216. package/src/route/router/route.ts +630 -0
  217. package/src/route/router/router.ts +1642 -0
  218. package/src/route/uri/hash.ts +308 -0
  219. package/src/route/uri/index.ts +7 -0
  220. package/src/route/uri/pathname.ts +376 -0
  221. package/src/route/uri/search.ts +413 -0
  222. package/src/singleton/README.md +79 -0
  223. package/src/singleton/factory.ts +55 -0
  224. package/src/singleton/index.ts +2 -0
  225. package/src/singleton/manager.ts +204 -0
  226. package/src/socket/README.md +105 -0
  227. package/src/socket/client/index.ts +2 -0
  228. package/src/socket/client/socket-unit.ts +660 -0
  229. package/src/socket/client/socket.ts +203 -0
  230. package/src/socket/common/index.ts +2 -0
  231. package/src/socket/common/socket-unit-common.ts +23 -0
  232. package/src/socket/common/socket-unit-heartbeat.ts +427 -0
  233. package/src/socket/index.ts +3 -0
  234. package/src/socket/server/index.ts +3 -0
  235. package/src/socket/server/server.ts +183 -0
  236. package/src/socket/server/socket-unit.ts +449 -0
  237. package/src/socket/server/socket.ts +264 -0
  238. package/src/storage/README.md +107 -0
  239. package/src/storage/index.ts +1 -0
  240. package/src/storage/table.ts +449 -0
  241. package/src/timer/README.md +86 -0
  242. package/src/timer/expiration/expiration-manager.ts +594 -0
  243. package/src/timer/expiration/index.ts +3 -0
  244. package/src/timer/expiration/min-heap.ts +208 -0
  245. package/src/timer/expiration/remaining-manager.ts +241 -0
  246. package/src/timer/index.ts +1 -0
  247. package/src/tube/README.md +99 -0
  248. package/src/tube/helper.ts +138 -0
  249. package/src/tube/index.ts +2 -0
  250. package/src/tube/tube.ts +880 -0
  251. package/src/type/README.md +54 -307
  252. package/src/type/class.ts +2 -2
  253. package/src/type/index.ts +14 -14
  254. package/src/type/is.ts +265 -2
  255. package/src/type/object.ts +37 -0
  256. package/src/type/string.ts +7 -2
  257. package/src/type/tuple.ts +6 -6
  258. package/src/type/union.ts +16 -0
  259. package/src/web/README.md +77 -0
  260. package/src/web/capture.ts +35 -0
  261. package/src/web/clipboard.ts +97 -0
  262. package/src/web/dom.ts +117 -0
  263. package/src/web/download.ts +16 -0
  264. package/src/web/event.ts +46 -0
  265. package/src/web/index.ts +10 -0
  266. package/src/web/local-storage.ts +113 -0
  267. package/src/web/location.ts +28 -0
  268. package/src/web/permission.ts +172 -0
  269. package/src/web/script-loader.ts +432 -0
  270. package/src/weixin/README.md +1 -0
  271. package/src/weixin/index.ts +2 -0
  272. package/src/weixin/official-account/authorization.ts +159 -0
  273. package/src/weixin/official-account/index.ts +2 -0
  274. package/src/weixin/official-account/js-api.ts +134 -0
  275. package/src/weixin/open/index.ts +1 -0
  276. package/src/weixin/open/oauth2.ts +133 -0
  277. package/tests/unit/abort/abort-manager.spec.ts +225 -0
  278. package/tests/unit/abort/abort-signal-listener-manager.spec.ts +62 -0
  279. package/tests/unit/ai/ai.spec.ts +85 -0
  280. package/tests/unit/aio/content.spec.ts +105 -0
  281. package/tests/unit/aio/json.spec.ts +147 -0
  282. package/tests/unit/aio/prompt.spec.ts +111 -0
  283. package/tests/unit/basic/array.spec.ts +1 -1
  284. package/tests/unit/basic/error.spec.ts +16 -4
  285. package/tests/unit/basic/schedule.spec.ts +74 -0
  286. package/tests/unit/basic/stream.spec.ts +91 -38
  287. package/tests/unit/basic/string.spec.ts +0 -9
  288. package/tests/unit/color/rgb/analyze.spec.ts +110 -0
  289. package/tests/unit/color/rgb/construct.spec.ts +56 -0
  290. package/tests/unit/color/rgb/convert.spec.ts +60 -0
  291. package/tests/unit/color/rgb/derive.spec.ts +103 -0
  292. package/tests/unit/color/rgb/parse.spec.ts +66 -0
  293. package/tests/unit/color/rgb/serialize.spec.ts +46 -0
  294. package/tests/unit/color/xyz/analyze.spec.ts +33 -0
  295. package/tests/unit/color/xyz/construct.spec.ts +10 -0
  296. package/tests/unit/color/xyz/convert.spec.ts +18 -0
  297. package/tests/unit/credential/api-key.spec.ts +37 -0
  298. package/tests/unit/credential/bearer.spec.ts +23 -0
  299. package/tests/unit/credential/json-web-token.spec.ts +23 -0
  300. package/tests/unit/credential/password.spec.ts +41 -0
  301. package/tests/unit/cron/cron.spec.ts +84 -0
  302. package/tests/unit/css/class.spec.ts +157 -0
  303. package/tests/unit/environment/basic.spec.ts +20 -0
  304. package/tests/unit/environment/device.spec.ts +146 -0
  305. package/tests/unit/environment/feature.spec.ts +388 -0
  306. package/tests/unit/environment/geo.spec.ts +111 -0
  307. package/tests/unit/environment/runtime.spec.ts +364 -0
  308. package/tests/unit/environment/snapshot.spec.ts +4 -0
  309. package/tests/unit/environment/variable.spec.ts +190 -0
  310. package/tests/unit/event/class-event-proxy.spec.ts +225 -0
  311. package/tests/unit/event/event-manager.spec.ts +246 -0
  312. package/tests/unit/event/instance-event-proxy.spec.ts +187 -0
  313. package/tests/unit/exception/browser.spec.ts +213 -0
  314. package/tests/unit/exception/nodejs.spec.ts +144 -0
  315. package/tests/unit/exception/normalize.spec.ts +57 -0
  316. package/tests/unit/form/inputor-controller/base.spec.ts +458 -0
  317. package/tests/unit/form/inputor-controller/boolean.spec.ts +30 -0
  318. package/tests/unit/form/inputor-controller/file.spec.ts +27 -0
  319. package/tests/unit/form/inputor-controller/form.spec.ts +120 -0
  320. package/tests/unit/form/inputor-controller/helper.spec.ts +67 -0
  321. package/tests/unit/form/inputor-controller/multi-select.spec.ts +34 -0
  322. package/tests/unit/form/inputor-controller/number.spec.ts +36 -0
  323. package/tests/unit/form/inputor-controller/select.spec.ts +49 -0
  324. package/tests/unit/form/inputor-controller/text.spec.ts +34 -0
  325. package/tests/unit/http/api/api-core-host.spec.ts +207 -0
  326. package/tests/unit/http/api/api-schema.spec.ts +120 -0
  327. package/tests/unit/http/api/api-server.spec.ts +363 -0
  328. package/tests/unit/http/api/api-test.spec.ts +117 -0
  329. package/tests/unit/http/api/api.spec.ts +121 -0
  330. package/tests/unit/http/api-adapter/node-http.spec.ts +191 -0
  331. package/tests/unit/identifier/id.spec.ts +71 -0
  332. package/tests/unit/identifier/uuid.spec.ts +85 -0
  333. package/tests/unit/json/repair.spec.ts +11 -0
  334. package/tests/unit/log/log-emitter.spec.ts +33 -0
  335. package/tests/unit/log/log-scheduler.spec.ts +40 -0
  336. package/tests/unit/log/log-type.spec.ts +7 -0
  337. package/tests/unit/log/logger.spec.ts +237 -0
  338. package/tests/unit/openai/openai.spec.ts +64 -0
  339. package/tests/unit/orchestration/coordination/barrier.spec.ts +96 -0
  340. package/tests/unit/orchestration/coordination/count-down-latch.spec.ts +63 -0
  341. package/tests/unit/orchestration/coordination/errors.spec.ts +29 -0
  342. package/tests/unit/orchestration/coordination/keyed-lock.spec.ts +109 -0
  343. package/tests/unit/orchestration/coordination/mutex.spec.ts +132 -0
  344. package/tests/unit/orchestration/coordination/permit.spec.ts +43 -0
  345. package/tests/unit/orchestration/coordination/read-write-lock.spec.ts +154 -0
  346. package/tests/unit/orchestration/coordination/semaphore.spec.ts +135 -0
  347. package/tests/unit/orchestration/dispatching/dispatcher.spec.ts +41 -0
  348. package/tests/unit/orchestration/dispatching/selector/down-count-selector.spec.ts +81 -0
  349. package/tests/unit/orchestration/scheduling/scheduler.spec.ts +103 -0
  350. package/tests/unit/random/base.spec.ts +58 -0
  351. package/tests/unit/random/random-boolean.spec.ts +25 -0
  352. package/tests/unit/random/random-integer.spec.ts +32 -0
  353. package/tests/unit/random/random-number.spec.ts +33 -0
  354. package/tests/unit/random/random-string.spec.ts +22 -0
  355. package/tests/unit/reactor/alien-signals-effect.spec.ts +11 -10
  356. package/tests/unit/reactor/preact-signal.spec.ts +1 -2
  357. package/tests/unit/request/fetch/browser.spec.ts +222 -0
  358. package/tests/unit/request/fetch/general.spec.ts +43 -0
  359. package/tests/unit/request/fetch/nodejs.spec.ts +225 -0
  360. package/tests/unit/request/request/base.spec.ts +385 -0
  361. package/tests/unit/request/request/general.spec.ts +161 -0
  362. package/tests/unit/route/router/route.spec.ts +431 -0
  363. package/tests/unit/route/router/router.spec.ts +407 -0
  364. package/tests/unit/route/uri/hash.spec.ts +72 -0
  365. package/tests/unit/route/uri/pathname.spec.ts +147 -0
  366. package/tests/unit/route/uri/search.spec.ts +107 -0
  367. package/tests/unit/singleton/singleton.spec.ts +49 -0
  368. package/tests/unit/socket/client.spec.ts +208 -0
  369. package/tests/unit/socket/server.spec.ts +135 -0
  370. package/tests/unit/socket/socket-unit-heartbeat.spec.ts +214 -0
  371. package/tests/unit/storage/table.spec.ts +620 -0
  372. package/tests/unit/timer/expiration/expiration-manager.spec.ts +464 -0
  373. package/tests/unit/timer/expiration/min-heap.spec.ts +71 -0
  374. package/tests/unit/timer/expiration/remaining-manager.spec.ts +234 -0
  375. package/tests/unit/tube/helper.spec.ts +139 -0
  376. package/tests/unit/tube/tube.spec.ts +501 -0
  377. package/.oxlintrc.json +0 -5
  378. package/src/random/uuid.ts +0 -103
  379. package/tests/unit/random/uuid.spec.ts +0 -37
@@ -0,0 +1,407 @@
1
+ import { expect, test } from "vitest"
2
+
3
+ import {
4
+ createRouter,
5
+ Router,
6
+ ROUTE_ACTION_TYPE,
7
+ ROUTE_TYPE,
8
+ } from "#Source/route/index.ts"
9
+
10
+ test("Router.getCurrentRouteHistoryEntry returns a defensive clone of the latest history entry", () => {
11
+ const router = createRouter({ url: "/home" })
12
+ const historyEntry = router.getCurrentRouteHistoryEntry()
13
+
14
+ historyEntry.id = "mutated"
15
+ historyEntry.to.route.partialUrl("/changed")
16
+
17
+ const latestHistoryEntry = router.getCurrentRouteHistoryEntry()
18
+
19
+ expect(latestHistoryEntry.type).toBe(ROUTE_TYPE.INITIALIZE)
20
+ expect(latestHistoryEntry.to.route.getRecord().partialUrl).toBe("/home")
21
+ expect(latestHistoryEntry.id).not.toBe("mutated")
22
+ })
23
+
24
+ test("Router.getCurrentRouteHistoryEntryId returns the latest history entry id", () => {
25
+ const router = createRouter({ url: "/home" })
26
+
27
+ expect(router.getCurrentRouteHistoryEntryId()).toBe(router.getCurrentRouteHistoryEntry().id)
28
+ })
29
+
30
+ test("Router.isRoaming reflects whether the current position is behind the tail", () => {
31
+ const router = createRouter({ url: "/home" })
32
+
33
+ expect(router.isRoaming()).toBe(false)
34
+ router.navigateTo({ url: "/projects" })
35
+ router.roamingBackwardBy({ step: 1 })
36
+
37
+ expect(router.isRoaming()).toBe(true)
38
+ })
39
+
40
+ test("Router.getMaxRoamingForwardSteps reports how many entries exist ahead of the current position", () => {
41
+ const router = createRouter({ url: "/home" })
42
+
43
+ router.navigateTo({ url: "/projects" })
44
+ router.navigateTo({ url: "/projects/alpha" })
45
+ router.roamingBackwardBy({ step: 1 })
46
+
47
+ expect(router.getMaxRoamingForwardSteps()).toBe(1)
48
+ })
49
+
50
+ test("Router.getMaxRoamingBackwardSteps reports how many entries exist behind the current position", () => {
51
+ const router = createRouter({ url: "/home" })
52
+
53
+ router.navigateTo({ url: "/projects" })
54
+ router.navigateTo({ url: "/projects/alpha" })
55
+ router.roamingBackwardBy({ step: 1 })
56
+
57
+ expect(router.getMaxRoamingBackwardSteps()).toBe(1)
58
+ })
59
+
60
+ test("Router.canRoamingForward reports whether forward roaming is possible", () => {
61
+ const router = createRouter({ url: "/home" })
62
+
63
+ expect(router.canRoamingForward()).toBe(false)
64
+ router.navigateTo({ url: "/projects" })
65
+ router.roamingBackwardBy({ step: 1 })
66
+
67
+ expect(router.canRoamingForward()).toBe(true)
68
+ })
69
+
70
+ test("Router.canRoamingBackward reports whether backward roaming is possible", () => {
71
+ const router = createRouter({ url: "/home" })
72
+
73
+ expect(router.canRoamingBackward()).toBe(false)
74
+ router.navigateTo({ url: "/projects" })
75
+
76
+ expect(router.canRoamingBackward()).toBe(true)
77
+ })
78
+
79
+ test("Router.refresh emits a refresh history entry without moving away from the current route", () => {
80
+ const router = createRouter({ url: "/home" })
81
+ const eventHistoryTypes: string[] = []
82
+
83
+ router.eventManager.subscribe("history", (historyEntry) => {
84
+ eventHistoryTypes.push(historyEntry.type)
85
+ })
86
+
87
+ router.refresh({})
88
+
89
+ const historyEntry = router.getCurrentRouteHistoryEntry()
90
+
91
+ expect(historyEntry.type).toBe(ROUTE_TYPE.REFRESH)
92
+ expect(historyEntry.to.route.getRecord().partialUrl).toBe("/home")
93
+ expect(historyEntry.actions[0]?.type).toBe(ROUTE_ACTION_TYPE.MOVE)
94
+ expect(eventHistoryTypes.at(-1)).toBe(ROUTE_TYPE.REFRESH)
95
+ })
96
+
97
+ test("Router.replaceTo replaces the current route in place", () => {
98
+ const router = createRouter({ url: "/home" })
99
+
100
+ router.replaceTo({ url: "/projects" })
101
+
102
+ const historyEntry = router.getCurrentRouteHistoryEntry()
103
+
104
+ expect(historyEntry.type).toBe(ROUTE_TYPE.REPLACE_TO)
105
+ expect(historyEntry.to.route.getRecord().partialUrl).toBe("/projects")
106
+ expect(historyEntry.actions.map((action) => action.type)).toEqual([ROUTE_ACTION_TYPE.REPLACE])
107
+ })
108
+
109
+ test("Router.switchTo moves to the tail trims forward history and replaces the tail route", () => {
110
+ const router = createRouter({ url: "/home" })
111
+
112
+ router.navigateTo({ url: "/projects" })
113
+ router.navigateTo({ url: "/projects/alpha" })
114
+ router.roamingBackwardBy({ step: 1 })
115
+ router.switchTo({ url: "/settings" })
116
+
117
+ const historyEntry = router.getCurrentRouteHistoryEntry()
118
+
119
+ expect(historyEntry.type).toBe(ROUTE_TYPE.SWITCH_TO)
120
+ expect(historyEntry.to.route.getRecord().partialUrl).toBe("/settings")
121
+ expect(historyEntry.actions.map((action) => action.type)).toEqual([
122
+ ROUTE_ACTION_TYPE.MOVE,
123
+ ROUTE_ACTION_TYPE.TRIM,
124
+ ROUTE_ACTION_TYPE.REPLACE,
125
+ ])
126
+ })
127
+
128
+ test("Router.navigateTo trims forward history and appends a new route entry", () => {
129
+ const router = createRouter({ url: "/home" })
130
+
131
+ router.navigateTo({ url: "/projects" })
132
+ router.roamingBackwardBy({ step: 1 })
133
+ router.navigateTo({ url: "/settings" })
134
+
135
+ const historyEntry = router.getCurrentRouteHistoryEntry()
136
+
137
+ expect(historyEntry.type).toBe(ROUTE_TYPE.NAVIGATE_TO)
138
+ expect(historyEntry.to.route.getRecord().partialUrl).toBe("/settings")
139
+ expect(historyEntry.actions.map((action) => action.type)).toEqual([
140
+ ROUTE_ACTION_TYPE.TRIM,
141
+ ROUTE_ACTION_TYPE.PUSH,
142
+ ])
143
+ })
144
+
145
+ test("Router.navigateSearch appends a new route entry with updated search", () => {
146
+ const router = createRouter({ url: "/projects" })
147
+
148
+ router.navigateSearch({ search: { page: "2", filter: "active" } })
149
+
150
+ const historyEntry = router.getCurrentRouteHistoryEntry()
151
+
152
+ expect(historyEntry.type).toBe(ROUTE_TYPE.NAVIGATE_SEARCH)
153
+ expect(historyEntry.to.route.getRecord().partialUrl).toBe("/projects?page=2&filter=active")
154
+ })
155
+
156
+ test("Router.navigateHash appends a new route entry with updated hash", () => {
157
+ const router = createRouter({ url: "/projects" })
158
+
159
+ router.navigateHash({ hash: { panel: "files" } })
160
+
161
+ const historyEntry = router.getCurrentRouteHistoryEntry()
162
+
163
+ expect(historyEntry.type).toBe(ROUTE_TYPE.NAVIGATE_HASH)
164
+ expect(historyEntry.to.route.getRecord().partialUrl).toBe("/projects#panel=files")
165
+ })
166
+
167
+ test("Router.redirectTo trims forward history and replaces the current route", () => {
168
+ const router = createRouter({ url: "/home" })
169
+
170
+ router.navigateTo({ url: "/projects" })
171
+ router.roamingBackwardBy({ step: 1 })
172
+ router.redirectTo({ url: "/settings" })
173
+
174
+ const historyEntry = router.getCurrentRouteHistoryEntry()
175
+
176
+ expect(historyEntry.type).toBe(ROUTE_TYPE.REDIRECT_TO)
177
+ expect(historyEntry.to.route.getRecord().partialUrl).toBe("/settings")
178
+ expect(historyEntry.actions.map((action) => action.type)).toEqual([
179
+ ROUTE_ACTION_TYPE.TRIM,
180
+ ROUTE_ACTION_TYPE.REPLACE,
181
+ ])
182
+ })
183
+
184
+ test("Router.redirectSearch replaces the current route with updated search", () => {
185
+ const router = createRouter({ url: "/projects?page=1" })
186
+
187
+ router.redirectSearch({ search: { page: "2", mode: "grid" } })
188
+
189
+ const historyEntry = router.getCurrentRouteHistoryEntry()
190
+
191
+ expect(historyEntry.type).toBe(ROUTE_TYPE.REDIRECT_SEARCH)
192
+ expect(historyEntry.to.route.getRecord().partialUrl).toBe("/projects?page=2&mode=grid")
193
+ })
194
+
195
+ test("Router.redirectHash replaces the current route with updated hash", () => {
196
+ const router = createRouter({ url: "/projects#panel=list" })
197
+
198
+ router.redirectHash({ hash: { panel: "files", tab: "info" } })
199
+
200
+ const historyEntry = router.getCurrentRouteHistoryEntry()
201
+
202
+ expect(historyEntry.type).toBe(ROUTE_TYPE.REDIRECT_HASH)
203
+ expect(historyEntry.to.route.getRecord().partialUrl).toBe("/projects#panel=files&tab=info")
204
+ })
205
+
206
+ test("Router.roamingBy moves relative to the current position without trimming history", () => {
207
+ const router = createRouter({ url: "/home" })
208
+
209
+ router.navigateTo({ url: "/projects" })
210
+ router.navigateTo({ url: "/settings" })
211
+ router.roamingBy({ step: -2 })
212
+
213
+ const historyEntry = router.getCurrentRouteHistoryEntry()
214
+
215
+ expect(historyEntry.type).toBe(ROUTE_TYPE.ROAMING_BY)
216
+ expect(historyEntry.to.route.getRecord().partialUrl).toBe("/home")
217
+ expect(router.canRoamingForward()).toBe(true)
218
+ })
219
+
220
+ test("Router.roamingForwardBy validates step input and moves forward without trimming history", () => {
221
+ const router = createRouter({ url: "/home" })
222
+
223
+ router.navigateTo({ url: "/projects" })
224
+ router.roamingBackwardBy({ step: 1 })
225
+ expect(() => router.roamingForwardBy({ step: -1 })).toThrow("Step must be a non-negative integer")
226
+
227
+ router.roamingForwardBy({ step: 1 })
228
+
229
+ expect(router.getCurrentRouteHistoryEntry().type).toBe(ROUTE_TYPE.ROAMING_FORWARD_BY)
230
+ expect(router.getCurrentRouteHistoryEntry().to.route.getRecord().partialUrl).toBe("/projects")
231
+ })
232
+
233
+ test("Router.roamingBackwardBy validates step input and moves backward without trimming history", () => {
234
+ const router = createRouter({ url: "/home" })
235
+
236
+ router.navigateTo({ url: "/projects" })
237
+ expect(() => router.roamingBackwardBy({ step: -1 })).toThrow("Step must be a non-negative integer")
238
+
239
+ router.roamingBackwardBy({ step: 1 })
240
+
241
+ expect(router.getCurrentRouteHistoryEntry().type).toBe(ROUTE_TYPE.ROAMING_BACKWARD_BY)
242
+ expect(router.getCurrentRouteHistoryEntry().to.route.getRecord().partialUrl).toBe("/home")
243
+ })
244
+
245
+ test("Router.roamingTo roams to the first matching entry across the whole history", () => {
246
+ const router = createRouter({ url: "/home" })
247
+
248
+ router.navigateTo({ url: "/projects" })
249
+ router.navigateTo({ url: "/settings" })
250
+ router.roamingTo({ predicate: (routeEntry) => routeEntry.route.getRecord().pathname === "/projects" })
251
+
252
+ const historyEntry = router.getCurrentRouteHistoryEntry()
253
+
254
+ expect(historyEntry.type).toBe(ROUTE_TYPE.ROAMING_TO)
255
+ expect(historyEntry.to.route.getRecord().partialUrl).toBe("/projects")
256
+ })
257
+
258
+ test("Router.roamingForwardTo roams only within entries ahead of the current position", () => {
259
+ const router = createRouter({ url: "/home" })
260
+
261
+ router.navigateTo({ url: "/projects" })
262
+ router.navigateTo({ url: "/settings" })
263
+ router.roamingBackwardBy({ step: 2 })
264
+ router.roamingForwardTo({ predicate: (routeEntry) => routeEntry.route.getRecord().pathname === "/settings" })
265
+
266
+ const historyEntry = router.getCurrentRouteHistoryEntry()
267
+
268
+ expect(historyEntry.type).toBe(ROUTE_TYPE.ROAMING_FORWARD_TO)
269
+ expect(historyEntry.to.route.getRecord().partialUrl).toBe("/settings")
270
+ })
271
+
272
+ test("Router.roamingBackwardTo roams only within entries behind the current position", () => {
273
+ const router = createRouter({ url: "/home" })
274
+
275
+ router.navigateTo({ url: "/projects" })
276
+ router.navigateTo({ url: "/settings" })
277
+ router.roamingBackwardTo({ predicate: (routeEntry) => routeEntry.route.getRecord().pathname === "/home" })
278
+
279
+ const historyEntry = router.getCurrentRouteHistoryEntry()
280
+
281
+ expect(historyEntry.type).toBe(ROUTE_TYPE.ROAMING_BACKWARD_TO)
282
+ expect(historyEntry.to.route.getRecord().partialUrl).toBe("/home")
283
+ })
284
+
285
+ test("Router.goBy moves relative to the current position and trims forward history", () => {
286
+ const router = createRouter({ url: "/home" })
287
+
288
+ router.navigateTo({ url: "/projects" })
289
+ router.navigateTo({ url: "/settings" })
290
+ router.goBy({ step: -1 })
291
+
292
+ const historyEntry = router.getCurrentRouteHistoryEntry()
293
+
294
+ expect(historyEntry.type).toBe(ROUTE_TYPE.GO_BY)
295
+ expect(historyEntry.to.route.getRecord().partialUrl).toBe("/projects")
296
+ expect(router.canRoamingForward()).toBe(false)
297
+ })
298
+
299
+ test("Router.goForwardBy validates step input and trims forward history at the target entry", () => {
300
+ const router = createRouter({ url: "/home" })
301
+
302
+ router.navigateTo({ url: "/projects" })
303
+ router.navigateTo({ url: "/settings" })
304
+ router.roamingBackwardBy({ step: 2 })
305
+ expect(() => router.goForwardBy({ step: -1 })).toThrow("Step must be a non-negative integer")
306
+
307
+ router.goForwardBy({ step: 1 })
308
+
309
+ expect(router.getCurrentRouteHistoryEntry().type).toBe(ROUTE_TYPE.GO_FORWARD_BY)
310
+ expect(router.getCurrentRouteHistoryEntry().to.route.getRecord().partialUrl).toBe("/projects")
311
+ expect(router.canRoamingForward()).toBe(false)
312
+ })
313
+
314
+ test("Router.goBackwardBy validates step input and trims forward history at the target entry", () => {
315
+ const router = createRouter({ url: "/home" })
316
+
317
+ router.navigateTo({ url: "/projects" })
318
+ router.navigateTo({ url: "/settings" })
319
+ expect(() => router.goBackwardBy({ step: -1 })).toThrow("Step must be a non-negative integer")
320
+
321
+ router.goBackwardBy({ step: 2 })
322
+
323
+ expect(router.getCurrentRouteHistoryEntry().type).toBe(ROUTE_TYPE.GO_BACKWARD_BY)
324
+ expect(router.getCurrentRouteHistoryEntry().to.route.getRecord().partialUrl).toBe("/home")
325
+ expect(router.canRoamingForward()).toBe(false)
326
+ })
327
+
328
+ test("Router.goTo moves to the first matching entry across the whole history and trims forward entries", () => {
329
+ const router = createRouter({ url: "/home" })
330
+
331
+ router.navigateTo({ url: "/projects" })
332
+ router.navigateTo({ url: "/settings" })
333
+ router.goTo({ predicate: (routeEntry) => routeEntry.route.getRecord().pathname === "/projects" })
334
+
335
+ const historyEntry = router.getCurrentRouteHistoryEntry()
336
+
337
+ expect(historyEntry.type).toBe(ROUTE_TYPE.GO_TO)
338
+ expect(historyEntry.to.route.getRecord().partialUrl).toBe("/projects")
339
+ expect(router.canRoamingForward()).toBe(false)
340
+ })
341
+
342
+ test("Router.goForwardTo moves to a matching later entry and trims any following entries", () => {
343
+ const router = createRouter({ url: "/home" })
344
+
345
+ router.navigateTo({ url: "/projects" })
346
+ router.navigateTo({ url: "/settings" })
347
+ router.navigateTo({ url: "/about" })
348
+ router.roamingBackwardBy({ step: 3 })
349
+ router.goForwardTo({ predicate: (routeEntry) => routeEntry.route.getRecord().pathname === "/settings" })
350
+
351
+ const historyEntry = router.getCurrentRouteHistoryEntry()
352
+
353
+ expect(historyEntry.type).toBe(ROUTE_TYPE.GO_FORWARD_TO)
354
+ expect(historyEntry.to.route.getRecord().partialUrl).toBe("/settings")
355
+ expect(router.canRoamingForward()).toBe(false)
356
+ })
357
+
358
+ test("Router.goBackwardTo moves to a matching earlier entry and trims any following entries", () => {
359
+ const router = createRouter({ url: "/home" })
360
+
361
+ router.navigateTo({ url: "/projects" })
362
+ router.navigateTo({ url: "/settings" })
363
+ router.goBackwardTo({ predicate: (routeEntry) => routeEntry.route.getRecord().pathname === "/home" })
364
+
365
+ const historyEntry = router.getCurrentRouteHistoryEntry()
366
+
367
+ expect(historyEntry.type).toBe(ROUTE_TYPE.GO_BACKWARD_TO)
368
+ expect(historyEntry.to.route.getRecord().partialUrl).toBe("/home")
369
+ expect(router.canRoamingForward()).toBe(false)
370
+ })
371
+
372
+ test("Router.roamingBackwardByOrRedirectTo roams backward when possible and otherwise redirects", () => {
373
+ const withHistory = createRouter({ url: "/home" })
374
+
375
+ withHistory.navigateTo({ url: "/projects" })
376
+ withHistory.roamingBackwardByOrRedirectTo({ step: 1, url: "/fallback" })
377
+ expect(withHistory.getCurrentRouteHistoryEntry().type).toBe(ROUTE_TYPE.ROAMING_BACKWARD_BY)
378
+ expect(withHistory.getCurrentRouteHistoryEntry().to.route.getRecord().partialUrl).toBe("/home")
379
+
380
+ const withoutHistory = createRouter({ url: "/home" })
381
+
382
+ withoutHistory.roamingBackwardByOrRedirectTo({ step: 1, url: "/fallback" })
383
+ expect(withoutHistory.getCurrentRouteHistoryEntry().type).toBe(ROUTE_TYPE.REDIRECT_TO)
384
+ expect(withoutHistory.getCurrentRouteHistoryEntry().to.route.getRecord().partialUrl).toBe("/fallback")
385
+ })
386
+
387
+ test("Router.goBackwardByOrRedirectTo goes backward when possible and otherwise redirects", () => {
388
+ const withHistory = createRouter({ url: "/home" })
389
+
390
+ withHistory.navigateTo({ url: "/projects" })
391
+ withHistory.goBackwardByOrRedirectTo({ step: 1, url: "/fallback" })
392
+ expect(withHistory.getCurrentRouteHistoryEntry().type).toBe(ROUTE_TYPE.GO_BACKWARD_BY)
393
+ expect(withHistory.getCurrentRouteHistoryEntry().to.route.getRecord().partialUrl).toBe("/home")
394
+
395
+ const withoutHistory = createRouter({ url: "/home" })
396
+
397
+ withoutHistory.goBackwardByOrRedirectTo({ step: 1, url: "/fallback" })
398
+ expect(withoutHistory.getCurrentRouteHistoryEntry().type).toBe(ROUTE_TYPE.REDIRECT_TO)
399
+ expect(withoutHistory.getCurrentRouteHistoryEntry().to.route.getRecord().partialUrl).toBe("/fallback")
400
+ })
401
+
402
+ test("createRouter creates a Router instance", () => {
403
+ const router = createRouter({ url: "/home" })
404
+
405
+ expect(router).toBeInstanceOf(Router)
406
+ expect(router.getCurrentRouteHistoryEntry().to.route.getRecord().partialUrl).toBe("/home")
407
+ })
@@ -0,0 +1,72 @@
1
+ import { expect, test } from "vitest"
2
+
3
+ import {
4
+ isHashIncludes,
5
+ isHashLooseEqual,
6
+ isHashLooseIncludes,
7
+ isHashStrictEqual,
8
+ isHashStrictIncludes,
9
+ hashObjectToHashString,
10
+ hashStringToHashObject,
11
+ neatenHash,
12
+ toHashObject,
13
+ toHashString,
14
+ } from "#Source/route/index.ts"
15
+
16
+ test("neatenHash ensures the hash string starts with a hash sign", () => {
17
+ expect(neatenHash("a=1&b=2&c=3")).toBe("#a=1&b=2&c=3")
18
+ expect(neatenHash("#a=1&b=2&c=3")).toBe("#a=1&b=2&c=3")
19
+ })
20
+
21
+ test("hashStringToHashObject decodes keys and values and preserves later equals signs", () => {
22
+ expect(hashStringToHashObject("#na%20me=mobius%20model&formula=a%3Db&empty="))
23
+ .toEqual({ "na me": "mobius model", formula: "a=b", empty: "" })
24
+ expect(hashStringToHashObject("#a=1&b=2&c=3")).toEqual({ a: "1", b: "2", c: "3" })
25
+ expect(hashStringToHashObject("#")).toEqual({})
26
+ })
27
+
28
+ test("hashObjectToHashString encodes keys and values into a hash string", () => {
29
+ expect(hashObjectToHashString({ "na me": "mobius model", formula: "a=b" }))
30
+ .toBe("#na%20me=mobius%20model&formula=a%3Db")
31
+ expect(hashObjectToHashString({ a: "1", b: "2", c: "3" })).toBe("#a=1&b=2&c=3")
32
+ expect(hashObjectToHashString({})).toBe("#")
33
+ })
34
+
35
+ test("toHashString returns hash format for both strings and objects", () => {
36
+ expect(toHashString("#a=1&b=2&c=3")).toBe("#a=1&b=2&c=3")
37
+ expect(toHashString({ a: "1", b: "2", c: "3" })).toBe("#a=1&b=2&c=3")
38
+ expect(toHashString({})).toBe("#")
39
+ })
40
+
41
+ test("toHashObject returns parsed hash objects for both strings and objects", () => {
42
+ const hashObject = { a: "1", b: "2", c: "3" }
43
+
44
+ expect(toHashObject("#a=1&b=2&c=3")).toEqual({ a: "1", b: "2", c: "3" })
45
+ expect(toHashObject(hashObject)).toBe(hashObject)
46
+ expect(toHashObject("#")).toEqual({})
47
+ })
48
+
49
+ test("isHashLooseIncludes checks whether all requested hash keys exist", () => {
50
+ expect(isHashLooseIncludes("#a=1&b=2", ["a"])).toBe(true)
51
+ expect(isHashLooseIncludes("#a=1&b=2", ["a", "c"])).toBe(false)
52
+ })
53
+
54
+ test("isHashStrictIncludes checks whether hash key sets are identical", () => {
55
+ expect(isHashStrictIncludes("#a=1&b=2", ["b", "a"])).toBe(true)
56
+ expect(isHashStrictIncludes("#ab=1&c=2", ["a", "bc"])).toBe(false)
57
+ })
58
+
59
+ test("isHashIncludes supports required and optional hash keys", () => {
60
+ expect(isHashIncludes("#a=1&b=2", { a: "required", b: "optional" })).toBe(true)
61
+ expect(isHashIncludes("#a=1&b=2", { a: "required" })).toBe(false)
62
+ })
63
+
64
+ test("isHashLooseEqual checks whether target hash values are a matching subset", () => {
65
+ expect(isHashLooseEqual("#a=1&b=2", "#a=1")).toBe(true)
66
+ expect(isHashLooseEqual("#a=1&b=2", { a: "1", c: "3" })).toBe(false)
67
+ })
68
+
69
+ test("isHashStrictEqual checks whether hash key sets and values are identical", () => {
70
+ expect(isHashStrictEqual("#a=1&b=2", "#b=2&a=1")).toBe(true)
71
+ expect(isHashStrictEqual("#a=1&b=2", "#a=1")).toBe(false)
72
+ })
@@ -0,0 +1,147 @@
1
+ import { expect, test } from "vitest"
2
+
3
+ import {
4
+ isPathnameEqual,
5
+ isPathnameLooseEqual,
6
+ isPathnameStrictEqual,
7
+ keepMinimumPathArray,
8
+ neatenPathname,
9
+ toPathnameArray,
10
+ toPathnameString,
11
+ removeInnerEmptyStringInArray,
12
+ removeConsecutiveRepetition,
13
+ removeConsecutiveRepetitionExcept,
14
+ removeConsecutiveRepetitionOf,
15
+ removeConsecutiveRepetitionOfEmpty,
16
+ removeConsecutiveRepetitionOfSlash,
17
+ } from "#Source/route/index.ts"
18
+
19
+ test("removeConsecutiveRepetition removes adjacent duplicate items", () => {
20
+ expect(removeConsecutiveRepetition(["", "", "app", "app", "page", "page", ""]))
21
+ .toEqual(["", "app", "page", ""])
22
+ expect(removeConsecutiveRepetition([])).toEqual([])
23
+ })
24
+
25
+ test("removeConsecutiveRepetitionOf removes adjacent duplicates only for selected values", () => {
26
+ expect(removeConsecutiveRepetitionOf(["", "", "app", "/", "/", "page", "page"], ["", "/"]))
27
+ .toEqual(["", "app", "/", "page", "page"])
28
+ })
29
+
30
+ test("removeConsecutiveRepetitionExcept keeps repeated exceptions and removes others", () => {
31
+ expect(removeConsecutiveRepetitionExcept(["", "", "app", "app", "page", "page", "", ""], [""]))
32
+ .toEqual(["", "", "app", "page", "", ""])
33
+ })
34
+
35
+ test("removeConsecutiveRepetitionOfEmpty removes only adjacent empty strings", () => {
36
+ expect(removeConsecutiveRepetitionOfEmpty(["", "", "app", "", "", "page", ""]))
37
+ .toEqual(["", "app", "", "page", ""])
38
+ })
39
+
40
+ test("removeConsecutiveRepetitionOfSlash removes only adjacent slash characters", () => {
41
+ expect(removeConsecutiveRepetitionOfSlash(["/", "/", "a", "/", "/", "/", "b"]))
42
+ .toEqual(["/", "a", "/", "b"])
43
+ })
44
+
45
+ test("removeInnerEmptyStringInArray keeps boundary empties and removes inner empties", () => {
46
+ expect(removeInnerEmptyStringInArray(["", "app", "", "page", ""]))
47
+ .toEqual(["", "app", "page", ""])
48
+ expect(removeInnerEmptyStringInArray(["", "", "app", "", "page", "", ""]))
49
+ .toEqual(["", "app", "page", ""])
50
+ })
51
+
52
+ test("keepMinimumPathArray preserves the minimal root pathname representation", () => {
53
+ expect(keepMinimumPathArray([""])).toEqual(["", ""])
54
+ expect(keepMinimumPathArray(["", "app"])).toEqual(["", "app"])
55
+ })
56
+
57
+ test("neatenPathname normalizes both array and string inputs", () => {
58
+ expect(neatenPathname(["app", "", "page", ""]))
59
+ .toEqual(["", "app", "page", ""])
60
+ expect(neatenPathname("//app//page//")).toBe("/app/page/")
61
+ })
62
+
63
+ test("toPathnameArray converts pathname inputs into normalized arrays", () => {
64
+ const cases: Array<[string | string[], string[]]> = [
65
+ [[""], ["", ""]],
66
+ [["", ""], ["", ""]],
67
+ [["app", "page"], ["", "app", "page"]],
68
+ [["", "app", "page"], ["", "app", "page"]],
69
+ [["", "app", "", "page"], ["", "app", "page"]],
70
+ [["", "app", "", "page", ""], ["", "app", "page", ""]],
71
+ [["", "", "app", "", "page", ""], ["", "app", "page", ""]],
72
+ [["", "", "app", "", "", "page", ""], ["", "app", "page", ""]],
73
+ [["", "", "app", "", "", "page", "", ""], ["", "app", "page", ""]],
74
+ ["", ["", ""]],
75
+ ["/", ["", ""]],
76
+ ["app/page", ["", "app", "page"]],
77
+ ["/app/page", ["", "app", "page"]],
78
+ ["/app/page/", ["", "app", "page", ""]],
79
+ ["//app/page/", ["", "app", "page", ""]],
80
+ ["//app//page/", ["", "app", "page", ""]],
81
+ ["//app//page//", ["", "app", "page", ""]],
82
+ ]
83
+
84
+ cases.forEach(([input, expected]) => {
85
+ expect(toPathnameArray(input)).toEqual(expected)
86
+ })
87
+ })
88
+
89
+ test("toPathnameString converts pathname inputs into normalized strings", () => {
90
+ const cases: Array<[string | string[], string]> = [
91
+ [[""], "/"],
92
+ [["", ""], "/"],
93
+ [["app", "page"], "/app/page"],
94
+ [["", "app", "page"], "/app/page"],
95
+ [["app", "", "page"], "/app/page"],
96
+ [["app", "page", ""], "/app/page/"],
97
+ [["", "app", "", "page", ""], "/app/page/"],
98
+ [["", "", "app", "", "", "page", "", ""], "/app/page/"],
99
+ ["", "/"],
100
+ ["/", "/"],
101
+ ["//", "/"],
102
+ ["/app/page", "/app/page"],
103
+ ["//app/page", "/app/page"],
104
+ ["//app//page", "/app/page"],
105
+ ["//app//page/", "/app/page/"],
106
+ ["//app//page//", "/app/page/"],
107
+ ]
108
+
109
+ cases.forEach(([input, expected]) => {
110
+ expect(toPathnameString(input)).toBe(expected)
111
+ })
112
+ })
113
+
114
+ test("isPathnameStrictEqual compares normalized pathnames with trailing slash sensitivity", () => {
115
+ const cases: Array<[string | string[], string | string[], boolean]> = [
116
+ ["", ["", ""], true],
117
+ ["/", ["", ""], true],
118
+ ["//app//page1//", ["", "app", "page1"], false],
119
+ ["//app//page1//", ["", "app", "page1", ""], true],
120
+ ]
121
+
122
+ cases.forEach(([left, right, expected]) => {
123
+ expect(isPathnameStrictEqual(left, right)).toBe(expected)
124
+ })
125
+ })
126
+
127
+ test("isPathnameLooseEqual ignores trailing slash differences after normalization", () => {
128
+ const cases: Array<[string | string[], string | string[], boolean]> = [
129
+ ["", ["", ""], true],
130
+ ["/", ["", ""], true],
131
+ ["//app//page1//", ["", "app", "page1"], true],
132
+ ["//app//page1//", ["", "app", "page1", ""], true],
133
+ ["/app/page", "/app/other/", false],
134
+ ]
135
+
136
+ cases.forEach(([left, right, expected]) => {
137
+ expect(isPathnameLooseEqual(left, right)).toBe(expected)
138
+ })
139
+ })
140
+
141
+ test("isPathnameEqual dispatches strict and loose modes and rejects invalid mode", () => {
142
+ expect(isPathnameEqual("strict", "/app/page", ["", "app", "page"])).toBe(true)
143
+ expect(isPathnameEqual("loose", "/app/page", "/app/page/")).toBe(true)
144
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
145
+ expect(() => isPathnameEqual("other" as "strict", "/app/page", "/app/page"))
146
+ .toThrow("\"mode\" must be \"strict\" or \"loose\".")
147
+ })