@bquery/bquery 1.5.0 → 1.7.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 (380) hide show
  1. package/README.md +193 -23
  2. package/dist/a11y/announce.d.ts +43 -0
  3. package/dist/a11y/announce.d.ts.map +1 -0
  4. package/dist/a11y/audit.d.ts +42 -0
  5. package/dist/a11y/audit.d.ts.map +1 -0
  6. package/dist/a11y/index.d.ts +53 -0
  7. package/dist/a11y/index.d.ts.map +1 -0
  8. package/dist/a11y/media-preferences.d.ts +77 -0
  9. package/dist/a11y/media-preferences.d.ts.map +1 -0
  10. package/dist/a11y/roving-tab-index.d.ts +38 -0
  11. package/dist/a11y/roving-tab-index.d.ts.map +1 -0
  12. package/dist/a11y/skip-link.d.ts +37 -0
  13. package/dist/a11y/skip-link.d.ts.map +1 -0
  14. package/dist/a11y/trap-focus.d.ts +49 -0
  15. package/dist/a11y/trap-focus.d.ts.map +1 -0
  16. package/dist/a11y/types.d.ts +152 -0
  17. package/dist/a11y/types.d.ts.map +1 -0
  18. package/dist/a11y-C5QOVvRn.js +421 -0
  19. package/dist/a11y-C5QOVvRn.js.map +1 -0
  20. package/dist/a11y.es.mjs +14 -0
  21. package/dist/component/component.d.ts +13 -5
  22. package/dist/component/component.d.ts.map +1 -1
  23. package/dist/component/html.d.ts +40 -3
  24. package/dist/component/html.d.ts.map +1 -1
  25. package/dist/component/index.d.ts +3 -2
  26. package/dist/component/index.d.ts.map +1 -1
  27. package/dist/component/library.d.ts.map +1 -1
  28. package/dist/component/scope.d.ts +138 -0
  29. package/dist/component/scope.d.ts.map +1 -0
  30. package/dist/component/types.d.ts +184 -17
  31. package/dist/component/types.d.ts.map +1 -1
  32. package/dist/component-CuuTijA6.js +684 -0
  33. package/dist/component-CuuTijA6.js.map +1 -0
  34. package/dist/component.es.mjs +10 -6
  35. package/dist/{config-DRmZZno3.js → config-BW35FKuA.js} +4 -4
  36. package/dist/config-BW35FKuA.js.map +1 -0
  37. package/dist/constraints-3lV9yyBw.js +100 -0
  38. package/dist/constraints-3lV9yyBw.js.map +1 -0
  39. package/dist/core/collection.d.ts +48 -0
  40. package/dist/core/collection.d.ts.map +1 -1
  41. package/dist/core/element.d.ts +92 -0
  42. package/dist/core/element.d.ts.map +1 -1
  43. package/dist/core/env.d.ts +18 -0
  44. package/dist/core/env.d.ts.map +1 -0
  45. package/dist/core/index.d.ts +1 -0
  46. package/dist/core/index.d.ts.map +1 -1
  47. package/dist/core/shared.d.ts +8 -0
  48. package/dist/core/shared.d.ts.map +1 -1
  49. package/dist/core/utils/index.d.ts +52 -41
  50. package/dist/core/utils/index.d.ts.map +1 -1
  51. package/dist/core-Cjl7GUu8.js +717 -0
  52. package/dist/core-Cjl7GUu8.js.map +1 -0
  53. package/dist/{core-DPdbItcq.js → core-DnlyjbF2.js} +1 -1
  54. package/dist/{core-DPdbItcq.js.map → core-DnlyjbF2.js.map} +1 -1
  55. package/dist/core.es.mjs +45 -44
  56. package/dist/custom-directives-7wAShnnd.js +9 -0
  57. package/dist/custom-directives-7wAShnnd.js.map +1 -0
  58. package/dist/devtools/devtools.d.ts +212 -0
  59. package/dist/devtools/devtools.d.ts.map +1 -0
  60. package/dist/devtools/index.d.ts +20 -0
  61. package/dist/devtools/index.d.ts.map +1 -0
  62. package/dist/devtools/types.d.ts +69 -0
  63. package/dist/devtools/types.d.ts.map +1 -0
  64. package/dist/devtools-D2fQLhDN.js +122 -0
  65. package/dist/devtools-D2fQLhDN.js.map +1 -0
  66. package/dist/devtools.es.mjs +19 -0
  67. package/dist/dnd/draggable.d.ts +51 -0
  68. package/dist/dnd/draggable.d.ts.map +1 -0
  69. package/dist/dnd/droppable.d.ts +38 -0
  70. package/dist/dnd/droppable.d.ts.map +1 -0
  71. package/dist/dnd/index.d.ts +47 -0
  72. package/dist/dnd/index.d.ts.map +1 -0
  73. package/dist/dnd/sortable.d.ts +43 -0
  74. package/dist/dnd/sortable.d.ts.map +1 -0
  75. package/dist/dnd/types.d.ts +250 -0
  76. package/dist/dnd/types.d.ts.map +1 -0
  77. package/dist/dnd-B8EgyzaI.js +244 -0
  78. package/dist/dnd-B8EgyzaI.js.map +1 -0
  79. package/dist/dnd.es.mjs +6 -0
  80. package/dist/env-NeVmr4Gf.js +19 -0
  81. package/dist/env-NeVmr4Gf.js.map +1 -0
  82. package/dist/forms/create-form.d.ts +49 -0
  83. package/dist/forms/create-form.d.ts.map +1 -0
  84. package/dist/forms/index.d.ts +39 -0
  85. package/dist/forms/index.d.ts.map +1 -0
  86. package/dist/forms/types.d.ts +139 -0
  87. package/dist/forms/types.d.ts.map +1 -0
  88. package/dist/forms/validators.d.ts +179 -0
  89. package/dist/forms/validators.d.ts.map +1 -0
  90. package/dist/forms-C3yovgH9.js +141 -0
  91. package/dist/forms-C3yovgH9.js.map +1 -0
  92. package/dist/forms.es.mjs +14 -0
  93. package/dist/full.d.ts +37 -9
  94. package/dist/full.d.ts.map +1 -1
  95. package/dist/full.es.mjs +186 -91
  96. package/dist/full.iife.js +47 -31
  97. package/dist/full.iife.js.map +1 -1
  98. package/dist/full.umd.js +47 -31
  99. package/dist/full.umd.js.map +1 -1
  100. package/dist/i18n/formatting.d.ts +40 -0
  101. package/dist/i18n/formatting.d.ts.map +1 -0
  102. package/dist/i18n/i18n.d.ts +48 -0
  103. package/dist/i18n/i18n.d.ts.map +1 -0
  104. package/dist/i18n/index.d.ts +57 -0
  105. package/dist/i18n/index.d.ts.map +1 -0
  106. package/dist/i18n/translate.d.ts +83 -0
  107. package/dist/i18n/translate.d.ts.map +1 -0
  108. package/dist/i18n/types.d.ts +156 -0
  109. package/dist/i18n/types.d.ts.map +1 -0
  110. package/dist/i18n-BnnhTFOS.js +89 -0
  111. package/dist/i18n-BnnhTFOS.js.map +1 -0
  112. package/dist/i18n.es.mjs +6 -0
  113. package/dist/index.d.ts +11 -0
  114. package/dist/index.d.ts.map +1 -1
  115. package/dist/index.es.mjs +233 -138
  116. package/dist/media/battery.d.ts +35 -0
  117. package/dist/media/battery.d.ts.map +1 -0
  118. package/dist/media/breakpoints.d.ts +51 -0
  119. package/dist/media/breakpoints.d.ts.map +1 -0
  120. package/dist/media/clipboard.d.ts +30 -0
  121. package/dist/media/clipboard.d.ts.map +1 -0
  122. package/dist/media/device-sensors.d.ts +54 -0
  123. package/dist/media/device-sensors.d.ts.map +1 -0
  124. package/dist/media/geolocation.d.ts +38 -0
  125. package/dist/media/geolocation.d.ts.map +1 -0
  126. package/dist/media/index.d.ts +42 -0
  127. package/dist/media/index.d.ts.map +1 -0
  128. package/dist/media/media-query.d.ts +36 -0
  129. package/dist/media/media-query.d.ts.map +1 -0
  130. package/dist/media/network.d.ts +35 -0
  131. package/dist/media/network.d.ts.map +1 -0
  132. package/dist/media/types.d.ts +173 -0
  133. package/dist/media/types.d.ts.map +1 -0
  134. package/dist/media/viewport.d.ts +32 -0
  135. package/dist/media/viewport.d.ts.map +1 -0
  136. package/dist/media-Di2Ta22s.js +340 -0
  137. package/dist/media-Di2Ta22s.js.map +1 -0
  138. package/dist/media.es.mjs +12 -0
  139. package/dist/motion/index.d.ts +7 -3
  140. package/dist/motion/index.d.ts.map +1 -1
  141. package/dist/motion/morph.d.ts +27 -0
  142. package/dist/motion/morph.d.ts.map +1 -0
  143. package/dist/motion/parallax.d.ts +30 -0
  144. package/dist/motion/parallax.d.ts.map +1 -0
  145. package/dist/motion/reduced-motion.d.ts +36 -3
  146. package/dist/motion/reduced-motion.d.ts.map +1 -1
  147. package/dist/motion/types.d.ts +58 -0
  148. package/dist/motion/types.d.ts.map +1 -1
  149. package/dist/motion/typewriter.d.ts +31 -0
  150. package/dist/motion/typewriter.d.ts.map +1 -0
  151. package/dist/motion-qPj_TYGv.js +530 -0
  152. package/dist/motion-qPj_TYGv.js.map +1 -0
  153. package/dist/motion.es.mjs +27 -23
  154. package/dist/mount-SM07RUa6.js +403 -0
  155. package/dist/mount-SM07RUa6.js.map +1 -0
  156. package/dist/{object-qGpWr6-J.js → object-BCk-1c8T.js} +5 -4
  157. package/dist/{object-qGpWr6-J.js.map → object-BCk-1c8T.js.map} +1 -1
  158. package/dist/{platform-B7JhGBc7.js → platform-CPbCprb6.js} +3 -3
  159. package/dist/platform-CPbCprb6.js.map +1 -0
  160. package/dist/platform.es.mjs +2 -2
  161. package/dist/plugin/index.d.ts +22 -0
  162. package/dist/plugin/index.d.ts.map +1 -0
  163. package/dist/plugin/registry.d.ts +108 -0
  164. package/dist/plugin/registry.d.ts.map +1 -0
  165. package/dist/plugin/types.d.ts +110 -0
  166. package/dist/plugin/types.d.ts.map +1 -0
  167. package/dist/plugin-cPoOHFLY.js +64 -0
  168. package/dist/plugin-cPoOHFLY.js.map +1 -0
  169. package/dist/plugin.es.mjs +9 -0
  170. package/dist/reactive/computed.d.ts +7 -0
  171. package/dist/reactive/computed.d.ts.map +1 -1
  172. package/dist/reactive-Cfv0RK6x.js +233 -0
  173. package/dist/reactive-Cfv0RK6x.js.map +1 -0
  174. package/dist/reactive.es.mjs +18 -17
  175. package/dist/registry-CWf368tT.js +26 -0
  176. package/dist/registry-CWf368tT.js.map +1 -0
  177. package/dist/router/bq-link.d.ts +112 -0
  178. package/dist/router/bq-link.d.ts.map +1 -0
  179. package/dist/router/constraints.d.ts +9 -0
  180. package/dist/router/constraints.d.ts.map +1 -0
  181. package/dist/router/index.d.ts +14 -6
  182. package/dist/router/index.d.ts.map +1 -1
  183. package/dist/router/match.d.ts +0 -1
  184. package/dist/router/match.d.ts.map +1 -1
  185. package/dist/router/path-pattern.d.ts +14 -0
  186. package/dist/router/path-pattern.d.ts.map +1 -0
  187. package/dist/router/query.d.ts.map +1 -1
  188. package/dist/router/router.d.ts +3 -1
  189. package/dist/router/router.d.ts.map +1 -1
  190. package/dist/router/types.d.ts +48 -4
  191. package/dist/router/types.d.ts.map +1 -1
  192. package/dist/router/use-route.d.ts +50 -0
  193. package/dist/router/use-route.d.ts.map +1 -0
  194. package/dist/router/utils.d.ts +3 -0
  195. package/dist/router/utils.d.ts.map +1 -1
  196. package/dist/router-BrthaP_z.js +473 -0
  197. package/dist/router-BrthaP_z.js.map +1 -0
  198. package/dist/router.es.mjs +13 -10
  199. package/dist/{sanitize-jyJ2ryE2.js → sanitize-B1V4JswB.js} +95 -83
  200. package/dist/sanitize-B1V4JswB.js.map +1 -0
  201. package/dist/security/index.d.ts +2 -0
  202. package/dist/security/index.d.ts.map +1 -1
  203. package/dist/security/sanitize.d.ts +4 -1
  204. package/dist/security/sanitize.d.ts.map +1 -1
  205. package/dist/security/trusted-html.d.ts +53 -0
  206. package/dist/security/trusted-html.d.ts.map +1 -0
  207. package/dist/security.es.mjs +10 -9
  208. package/dist/ssr/hydrate.d.ts +65 -0
  209. package/dist/ssr/hydrate.d.ts.map +1 -0
  210. package/dist/ssr/index.d.ts +59 -0
  211. package/dist/ssr/index.d.ts.map +1 -0
  212. package/dist/ssr/render.d.ts +62 -0
  213. package/dist/ssr/render.d.ts.map +1 -0
  214. package/dist/ssr/serialize.d.ts +118 -0
  215. package/dist/ssr/serialize.d.ts.map +1 -0
  216. package/dist/ssr/types.d.ts +70 -0
  217. package/dist/ssr/types.d.ts.map +1 -0
  218. package/dist/ssr-B2qd_WBB.js +248 -0
  219. package/dist/ssr-B2qd_WBB.js.map +1 -0
  220. package/dist/ssr.es.mjs +9 -0
  221. package/dist/store/create-store.d.ts.map +1 -1
  222. package/dist/store/define-store.d.ts +1 -1
  223. package/dist/store/define-store.d.ts.map +1 -1
  224. package/dist/store/index.d.ts +1 -1
  225. package/dist/store/index.d.ts.map +1 -1
  226. package/dist/store/mapping.d.ts +1 -1
  227. package/dist/store/mapping.d.ts.map +1 -1
  228. package/dist/store/persisted.d.ts +38 -4
  229. package/dist/store/persisted.d.ts.map +1 -1
  230. package/dist/store/types.d.ts +140 -3
  231. package/dist/store/types.d.ts.map +1 -1
  232. package/dist/store/utils.d.ts +2 -2
  233. package/dist/store/utils.d.ts.map +1 -1
  234. package/dist/store/watch.d.ts +1 -1
  235. package/dist/store/watch.d.ts.map +1 -1
  236. package/dist/store-DWpyH6p5.js +338 -0
  237. package/dist/store-DWpyH6p5.js.map +1 -0
  238. package/dist/store.es.mjs +11 -10
  239. package/dist/storybook/index.d.ts +37 -0
  240. package/dist/storybook/index.d.ts.map +1 -0
  241. package/dist/storybook.es.mjs +151 -0
  242. package/dist/storybook.es.mjs.map +1 -0
  243. package/dist/testing/index.d.ts +23 -0
  244. package/dist/testing/index.d.ts.map +1 -0
  245. package/dist/testing/testing.d.ts +156 -0
  246. package/dist/testing/testing.d.ts.map +1 -0
  247. package/dist/testing/types.d.ts +134 -0
  248. package/dist/testing/types.d.ts.map +1 -0
  249. package/dist/testing-CsqjNUyy.js +224 -0
  250. package/dist/testing-CsqjNUyy.js.map +1 -0
  251. package/dist/testing.es.mjs +9 -0
  252. package/dist/type-guards-Do9DWgNp.js +44 -0
  253. package/dist/type-guards-Do9DWgNp.js.map +1 -0
  254. package/dist/untrack-DJVQQ2WM.js +33 -0
  255. package/dist/untrack-DJVQQ2WM.js.map +1 -0
  256. package/dist/view/custom-directives.d.ts +20 -0
  257. package/dist/view/custom-directives.d.ts.map +1 -0
  258. package/dist/view/evaluate.d.ts.map +1 -1
  259. package/dist/view/process.d.ts.map +1 -1
  260. package/dist/view.es.mjs +11 -10
  261. package/package.json +52 -11
  262. package/src/a11y/announce.ts +131 -0
  263. package/src/a11y/audit.ts +314 -0
  264. package/src/a11y/index.ts +68 -0
  265. package/src/a11y/media-preferences.ts +255 -0
  266. package/src/a11y/roving-tab-index.ts +164 -0
  267. package/src/a11y/skip-link.ts +255 -0
  268. package/src/a11y/trap-focus.ts +184 -0
  269. package/src/a11y/types.ts +183 -0
  270. package/src/component/component.ts +345 -65
  271. package/src/component/html.ts +153 -53
  272. package/src/component/index.ts +12 -2
  273. package/src/component/library.ts +66 -28
  274. package/src/component/scope.ts +212 -0
  275. package/src/component/types.ts +238 -19
  276. package/src/core/collection.ts +707 -628
  277. package/src/core/element.ts +981 -774
  278. package/src/core/env.ts +60 -0
  279. package/src/core/index.ts +49 -48
  280. package/src/core/shared.ts +62 -13
  281. package/src/core/utils/index.ts +148 -83
  282. package/src/devtools/devtools.ts +410 -0
  283. package/src/devtools/index.ts +48 -0
  284. package/src/devtools/types.ts +104 -0
  285. package/src/dnd/draggable.ts +296 -0
  286. package/src/dnd/droppable.ts +228 -0
  287. package/src/dnd/index.ts +62 -0
  288. package/src/dnd/sortable.ts +307 -0
  289. package/src/dnd/types.ts +293 -0
  290. package/src/forms/create-form.ts +278 -0
  291. package/src/forms/index.ts +65 -0
  292. package/src/forms/types.ts +154 -0
  293. package/src/forms/validators.ts +265 -0
  294. package/src/full.ts +260 -3
  295. package/src/i18n/formatting.ts +67 -0
  296. package/src/i18n/i18n.ts +200 -0
  297. package/src/i18n/index.ts +67 -0
  298. package/src/i18n/translate.ts +182 -0
  299. package/src/i18n/types.ts +171 -0
  300. package/src/index.ts +108 -36
  301. package/src/media/battery.ts +116 -0
  302. package/src/media/breakpoints.ts +131 -0
  303. package/src/media/clipboard.ts +80 -0
  304. package/src/media/device-sensors.ts +158 -0
  305. package/src/media/geolocation.ts +119 -0
  306. package/src/media/index.ts +76 -0
  307. package/src/media/media-query.ts +92 -0
  308. package/src/media/network.ts +115 -0
  309. package/src/media/types.ts +177 -0
  310. package/src/media/viewport.ts +84 -0
  311. package/src/motion/index.ts +57 -48
  312. package/src/motion/morph.ts +151 -0
  313. package/src/motion/parallax.ts +120 -0
  314. package/src/motion/reduced-motion.ts +66 -17
  315. package/src/motion/transition.ts +97 -97
  316. package/src/motion/types.ts +63 -0
  317. package/src/motion/typewriter.ts +164 -0
  318. package/src/platform/announcer.ts +208 -208
  319. package/src/platform/config.ts +163 -163
  320. package/src/platform/cookies.ts +165 -165
  321. package/src/platform/index.ts +39 -39
  322. package/src/platform/meta.ts +168 -168
  323. package/src/plugin/index.ts +37 -0
  324. package/src/plugin/registry.ts +269 -0
  325. package/src/plugin/types.ts +137 -0
  326. package/src/reactive/async-data.ts +486 -486
  327. package/src/reactive/computed.ts +130 -92
  328. package/src/reactive/index.ts +37 -37
  329. package/src/reactive/signal.ts +29 -29
  330. package/src/router/bq-link.ts +279 -0
  331. package/src/router/constraints.ts +201 -0
  332. package/src/router/index.ts +49 -41
  333. package/src/router/match.ts +312 -106
  334. package/src/router/path-pattern.ts +52 -0
  335. package/src/router/query.ts +38 -35
  336. package/src/router/router.ts +402 -211
  337. package/src/router/types.ts +139 -93
  338. package/src/router/use-route.ts +68 -0
  339. package/src/router/utils.ts +157 -116
  340. package/src/security/constants.ts +211 -211
  341. package/src/security/index.ts +12 -10
  342. package/src/security/sanitize.ts +6 -2
  343. package/src/security/trusted-html.ts +71 -0
  344. package/src/ssr/hydrate.ts +82 -0
  345. package/src/ssr/index.ts +70 -0
  346. package/src/ssr/render.ts +508 -0
  347. package/src/ssr/serialize.ts +296 -0
  348. package/src/ssr/types.ts +81 -0
  349. package/src/store/create-store.ts +467 -329
  350. package/src/store/define-store.ts +2 -1
  351. package/src/store/index.ts +27 -22
  352. package/src/store/mapping.ts +2 -1
  353. package/src/store/persisted.ts +249 -61
  354. package/src/store/types.ts +247 -94
  355. package/src/store/utils.ts +135 -141
  356. package/src/store/watch.ts +2 -1
  357. package/src/storybook/index.ts +480 -0
  358. package/src/testing/index.ts +42 -0
  359. package/src/testing/testing.ts +593 -0
  360. package/src/testing/types.ts +170 -0
  361. package/src/view/custom-directives.ts +30 -0
  362. package/src/view/evaluate.ts +292 -290
  363. package/src/view/process.ts +108 -92
  364. package/dist/component-CY5MVoYN.js +0 -531
  365. package/dist/component-CY5MVoYN.js.map +0 -1
  366. package/dist/config-DRmZZno3.js.map +0 -1
  367. package/dist/core-CK2Mfpf4.js +0 -648
  368. package/dist/core-CK2Mfpf4.js.map +0 -1
  369. package/dist/motion-C5DRdPnO.js +0 -415
  370. package/dist/motion-C5DRdPnO.js.map +0 -1
  371. package/dist/platform-B7JhGBc7.js.map +0 -1
  372. package/dist/reactive-BDya-ia8.js +0 -253
  373. package/dist/reactive-BDya-ia8.js.map +0 -1
  374. package/dist/router-CijiICxt.js +0 -188
  375. package/dist/router-CijiICxt.js.map +0 -1
  376. package/dist/sanitize-jyJ2ryE2.js.map +0 -1
  377. package/dist/store-CPK9E62U.js +0 -262
  378. package/dist/store-CPK9E62U.js.map +0 -1
  379. package/dist/view-Cdi0g-qo.js +0 -396
  380. package/dist/view-Cdi0g-qo.js.map +0 -1
@@ -0,0 +1,593 @@
1
+ /**
2
+ * Testing utilities for bQuery.js.
3
+ *
4
+ * Provides helpers for mounting components, controlling signals, mocking
5
+ * the router, dispatching events, and asserting async conditions — all
6
+ * designed for use with `bun:test` and happy-dom.
7
+ *
8
+ * @module bquery/testing
9
+ */
10
+
11
+ import { batch, Signal, signal } from '../reactive/index';
12
+ import { getNormalizedRouteConstraint } from '../router/constraints';
13
+ import type {
14
+ FireEventOptions,
15
+ MockRouteDefinition,
16
+ MockRouter,
17
+ MockRouterOptions,
18
+ MockSignal,
19
+ RenderComponentOptions,
20
+ RenderResult,
21
+ TestRoute,
22
+ WaitForOptions,
23
+ } from './types';
24
+
25
+ // ============================================================================
26
+ // renderComponent
27
+ // ============================================================================
28
+
29
+ const isWordChar = (char: string | undefined): boolean =>
30
+ char !== undefined &&
31
+ ((char >= 'a' && char <= 'z') ||
32
+ (char >= 'A' && char <= 'Z') ||
33
+ (char >= '0' && char <= '9') ||
34
+ char === '_');
35
+
36
+ const readRouteConstraint = (
37
+ pattern: string,
38
+ startIndex: number
39
+ ): { constraint: string; endIndex: number } | null => {
40
+ let depth = 1;
41
+ let constraint = '';
42
+ let i = startIndex + 1;
43
+
44
+ while (i < pattern.length) {
45
+ const char = pattern[i];
46
+
47
+ if (char === '\\' && i + 1 < pattern.length) {
48
+ constraint += char + pattern[i + 1];
49
+ i += 2;
50
+ continue;
51
+ }
52
+
53
+ if (char === '(') {
54
+ depth++;
55
+ } else if (char === ')') {
56
+ depth--;
57
+ if (depth === 0) {
58
+ return { constraint, endIndex: i + 1 };
59
+ }
60
+ }
61
+
62
+ constraint += char;
63
+ i++;
64
+ }
65
+
66
+ return null;
67
+ };
68
+
69
+ const routeConstraintRegexCache = new Map<string, RegExp>();
70
+
71
+ const getRouteConstraintRegex = (constraint: string): RegExp => {
72
+ const normalized = getNormalizedRouteConstraint(constraint);
73
+ const cached = routeConstraintRegexCache.get(normalized);
74
+ if (cached) {
75
+ return cached;
76
+ }
77
+
78
+ const compiled = new RegExp(`^(?:${normalized})$`);
79
+ routeConstraintRegexCache.set(normalized, compiled);
80
+ return compiled;
81
+ };
82
+
83
+ /**
84
+ * Mounts a custom element by tag name for testing and returns a handle
85
+ * to interact with it.
86
+ *
87
+ * The element is created, configured with the given props and slots,
88
+ * and appended to the container (defaults to `document.body`). Call
89
+ * `unmount()` to remove the element and trigger its `disconnectedCallback`.
90
+ *
91
+ * @param tagName - The custom element tag name (must already be registered)
92
+ * @param options - Props, slots, and container configuration
93
+ * @returns A {@link RenderResult} with the element and an unmount function
94
+ * @throws {Error} If the tag name is not a valid custom element name
95
+ *
96
+ * @example
97
+ * ```ts
98
+ * import { renderComponent } from '@bquery/bquery/testing';
99
+ *
100
+ * const { el, unmount } = renderComponent('my-counter', {
101
+ * props: { start: '5' },
102
+ * });
103
+ * expect(el.shadowRoot?.textContent).toContain('5');
104
+ * unmount();
105
+ * ```
106
+ */
107
+ export function renderComponent(
108
+ tagName: string,
109
+ options: RenderComponentOptions = {}
110
+ ): RenderResult {
111
+ if (!tagName || !tagName.includes('-')) {
112
+ throw new Error(
113
+ `bQuery testing: "${tagName}" is not a valid custom element tag name (must contain a hyphen)`
114
+ );
115
+ }
116
+
117
+ const { props, slots, container = document.body } = options;
118
+
119
+ const el = document.createElement(tagName);
120
+
121
+ // Set attributes (props) before connecting
122
+ if (props) {
123
+ for (const [key, value] of Object.entries(props)) {
124
+ if (value === null || value === undefined) continue;
125
+ el.setAttribute(key, String(value));
126
+ }
127
+ }
128
+
129
+ // Inject slot content before connecting so the component can discover it
130
+ if (slots) {
131
+ if (typeof slots === 'string') {
132
+ el.innerHTML = slots;
133
+ } else {
134
+ const parts: string[] = [];
135
+ for (const [slotName, html] of Object.entries(slots)) {
136
+ if (slotName === 'default') {
137
+ parts.push(html);
138
+ } else {
139
+ const safeSlotName = slotName
140
+ .replace(/&/g, '&amp;')
141
+ .replace(/"/g, '&quot;')
142
+ .replace(/</g, '&lt;')
143
+ .replace(/>/g, '&gt;');
144
+ parts.push(`<div slot="${safeSlotName}">${html}</div>`);
145
+ }
146
+ }
147
+ el.innerHTML = parts.join('');
148
+ }
149
+ }
150
+
151
+ // Connect — triggers connectedCallback
152
+ container.appendChild(el);
153
+
154
+ const unmount = (): void => {
155
+ if (el.parentNode) {
156
+ el.parentNode.removeChild(el);
157
+ }
158
+ };
159
+
160
+ return { el, unmount };
161
+ }
162
+
163
+ // ============================================================================
164
+ // flushEffects
165
+ // ============================================================================
166
+
167
+ /**
168
+ * Synchronously flushes any pending reactive effects.
169
+ *
170
+ * In bQuery's reactive system, effects outside of a batch are executed
171
+ * synchronously. This helper exists primarily for clarity and for
172
+ * flushing effects that may have been deferred inside a batch.
173
+ *
174
+ * Internally it performs a no-op batch to trigger the flush of any
175
+ * pending observers that were queued during a prior `batch()` call.
176
+ *
177
+ * @example
178
+ * ```ts
179
+ * import { signal, batch } from '@bquery/bquery/reactive';
180
+ * import { flushEffects } from '@bquery/bquery/testing';
181
+ *
182
+ * const count = signal(0);
183
+ * let observed = 0;
184
+ * effect(() => { observed = count.value; });
185
+ *
186
+ * batch(() => { count.value = 42; });
187
+ * flushEffects();
188
+ * expect(observed).toBe(42);
189
+ * ```
190
+ */
191
+ export function flushEffects(): void {
192
+ // A no-op batch triggers endBatch which flushes any pending observers.
193
+ // Since bQuery's effects are synchronous outside of batches, this is
194
+ // mainly useful after manual batch calls or micro-task boundaries.
195
+ batch(() => {
196
+ /* intentionally empty — triggers pending observer flush */
197
+ });
198
+ }
199
+
200
+ // ============================================================================
201
+ // mockSignal
202
+ // ============================================================================
203
+
204
+ /**
205
+ * Creates a controllable signal for tests with `set()` and `reset()` helpers.
206
+ *
207
+ * This is a thin wrapper around `signal()` that records the initial value
208
+ * and adds explicit `set()` / `reset()` methods for clearer test intent.
209
+ *
210
+ * @template T - The type of the signal value
211
+ * @param initialValue - The initial value
212
+ * @returns A {@link MockSignal} instance
213
+ *
214
+ * @example
215
+ * ```ts
216
+ * import { mockSignal } from '@bquery/bquery/testing';
217
+ *
218
+ * const count = mockSignal(0);
219
+ * count.set(5);
220
+ * expect(count.value).toBe(5);
221
+ * count.reset();
222
+ * expect(count.value).toBe(0);
223
+ * ```
224
+ */
225
+ export function mockSignal<T>(initialValue: T): MockSignal<T> {
226
+ const s = signal(initialValue) as Signal<T> & {
227
+ set: (value: T) => void;
228
+ reset: () => void;
229
+ initialValue: T;
230
+ };
231
+
232
+ Object.defineProperty(s, 'initialValue', {
233
+ value: initialValue,
234
+ writable: false,
235
+ enumerable: true,
236
+ });
237
+
238
+ s.set = function (value: T): void {
239
+ s.value = value;
240
+ };
241
+
242
+ s.reset = function (): void {
243
+ s.value = initialValue;
244
+ };
245
+
246
+ return s as MockSignal<T>;
247
+ }
248
+
249
+ // ============================================================================
250
+ // mockRouter
251
+ // ============================================================================
252
+
253
+ /**
254
+ * Parses a path string into the route's `path`, `query`, and `hash` parts.
255
+ * @internal
256
+ */
257
+ function parsePath(
258
+ fullPath: string,
259
+ base: string
260
+ ): { path: string; query: Record<string, string | string[]>; hash: string } {
261
+ let working = fullPath;
262
+
263
+ // Strip base prefix
264
+ if (base && working.startsWith(base)) {
265
+ working = working.slice(base.length) || '/';
266
+ }
267
+
268
+ // Extract hash
269
+ let hash = '';
270
+ const hashIdx = working.indexOf('#');
271
+ if (hashIdx >= 0) {
272
+ hash = working.slice(hashIdx + 1);
273
+ working = working.slice(0, hashIdx);
274
+ }
275
+
276
+ // Extract query string
277
+ const query: Record<string, string | string[]> = {};
278
+ const qIdx = working.indexOf('?');
279
+ if (qIdx >= 0) {
280
+ const qs = working.slice(qIdx + 1);
281
+ working = working.slice(0, qIdx);
282
+ for (const pair of qs.split('&')) {
283
+ const eqIdx = pair.indexOf('=');
284
+ const key = eqIdx >= 0 ? decodeURIComponent(pair.slice(0, eqIdx)) : decodeURIComponent(pair);
285
+ const val = eqIdx >= 0 ? decodeURIComponent(pair.slice(eqIdx + 1)) : '';
286
+ const existing = query[key];
287
+ if (existing !== undefined) {
288
+ if (Array.isArray(existing)) {
289
+ existing.push(val);
290
+ } else {
291
+ query[key] = [existing, val];
292
+ }
293
+ } else {
294
+ query[key] = val;
295
+ }
296
+ }
297
+ }
298
+
299
+ return { path: working || '/', query, hash };
300
+ }
301
+
302
+ /**
303
+ * Matches a path against a route definition, extracting params.
304
+ * @internal
305
+ */
306
+ function matchRoute(
307
+ path: string,
308
+ routes: MockRouteDefinition[]
309
+ ): { matched: MockRouteDefinition | null; params: Record<string, string> } {
310
+ for (const route of routes) {
311
+ const params = matchRoutePattern(route.path, path);
312
+ if (params) {
313
+ return { matched: route, params };
314
+ }
315
+ }
316
+ return { matched: null, params: {} };
317
+ }
318
+
319
+ /**
320
+ * Builds param matches from a route path pattern without compiling the full path into a regex.
321
+ * @internal
322
+ */
323
+ function matchRoutePattern(pattern: string, path: string): Record<string, string> | null {
324
+ if (pattern === '*') {
325
+ return {};
326
+ }
327
+
328
+ // Memoization keeps wildcard/param backtracking linear for repeated subproblems
329
+ // within a single pattern/path match attempt.
330
+ const memo = new Map<string, Record<string, string> | null>();
331
+
332
+ const findSegmentBoundary = (value: string, startIndex: number): number => {
333
+ const slashIndex = value.indexOf('/', startIndex);
334
+ return slashIndex === -1 ? value.length : slashIndex;
335
+ };
336
+
337
+ const matchFrom = (patternIndex: number, pathIndex: number): Record<string, string> | null => {
338
+ const memoKey = `${patternIndex}:${pathIndex}`;
339
+ if (memo.has(memoKey)) {
340
+ return memo.get(memoKey) ?? null;
341
+ }
342
+
343
+ if (patternIndex === pattern.length) {
344
+ const result = pathIndex === path.length ? {} : null;
345
+ memo.set(memoKey, result);
346
+ return result;
347
+ }
348
+
349
+ const patternChar = pattern[patternIndex];
350
+
351
+ if (patternChar === '*') {
352
+ for (let candidateEnd = path.length; candidateEnd >= pathIndex; candidateEnd--) {
353
+ const suffixMatch = matchFrom(patternIndex + 1, candidateEnd);
354
+ if (suffixMatch) {
355
+ memo.set(memoKey, suffixMatch);
356
+ return suffixMatch;
357
+ }
358
+ }
359
+
360
+ memo.set(memoKey, null);
361
+ return null;
362
+ }
363
+
364
+ if (patternChar === ':' && isWordChar(pattern[patternIndex + 1])) {
365
+ let nameEnd = patternIndex + 2;
366
+ while (nameEnd < pattern.length && isWordChar(pattern[nameEnd])) {
367
+ nameEnd++;
368
+ }
369
+
370
+ const name = pattern.slice(patternIndex + 1, nameEnd);
371
+ let nextPatternIndex = nameEnd;
372
+ let constraint: string | undefined;
373
+ let catchAll = false;
374
+
375
+ if (pattern[nameEnd] === '(') {
376
+ const parsedConstraint = readRouteConstraint(pattern, nameEnd);
377
+ if (parsedConstraint) {
378
+ constraint = parsedConstraint.constraint;
379
+ nextPatternIndex = parsedConstraint.endIndex;
380
+ }
381
+ }
382
+
383
+ if (pattern[nextPatternIndex] === '*') {
384
+ catchAll = true;
385
+ nextPatternIndex++;
386
+ }
387
+
388
+ const candidateLimit = catchAll
389
+ ? path.length
390
+ : constraint
391
+ ? path.length
392
+ : findSegmentBoundary(path, pathIndex);
393
+
394
+ for (let candidateEnd = candidateLimit; candidateEnd > pathIndex; candidateEnd--) {
395
+ const candidateValue = path.slice(pathIndex, candidateEnd);
396
+ if (constraint) {
397
+ const constraintRegex = getRouteConstraintRegex(constraint);
398
+ if (!constraintRegex.test(candidateValue)) {
399
+ continue;
400
+ }
401
+ }
402
+
403
+ const suffixMatch = matchFrom(nextPatternIndex, candidateEnd);
404
+ if (suffixMatch) {
405
+ const result = {
406
+ [name]: candidateValue,
407
+ ...suffixMatch,
408
+ };
409
+ memo.set(memoKey, result);
410
+ return result;
411
+ }
412
+ }
413
+
414
+ memo.set(memoKey, null);
415
+ return null;
416
+ }
417
+
418
+ if (pathIndex >= path.length || patternChar !== path[pathIndex]) {
419
+ memo.set(memoKey, null);
420
+ return null;
421
+ }
422
+
423
+ const result = matchFrom(patternIndex + 1, pathIndex + 1);
424
+ memo.set(memoKey, result);
425
+ return result;
426
+ };
427
+
428
+ return matchFrom(0, 0);
429
+ }
430
+
431
+ /**
432
+ * Creates a lightweight mock router for testing that does not interact
433
+ * with the browser History API.
434
+ *
435
+ * The mock router provides a reactive `currentRoute` signal that updates
436
+ * when `push()` or `replace()` is called, making it ideal for testing
437
+ * components or logic that depend on route state.
438
+ *
439
+ * @param options - Mock router configuration
440
+ * @returns A {@link MockRouter} instance
441
+ *
442
+ * @example
443
+ * ```ts
444
+ * import { mockRouter } from '@bquery/bquery/testing';
445
+ *
446
+ * const router = mockRouter({
447
+ * routes: [
448
+ * { path: '/', component: () => null },
449
+ * { path: '/user/:id', component: () => null },
450
+ * ],
451
+ * initialPath: '/',
452
+ * });
453
+ *
454
+ * router.push('/user/42');
455
+ * expect(router.currentRoute.value.params.id).toBe('42');
456
+ * router.destroy();
457
+ * ```
458
+ */
459
+ export function mockRouter(options: MockRouterOptions = {}): MockRouter {
460
+ const routes = options.routes ?? [{ path: '*', component: () => null }];
461
+ const base = options.base ?? '';
462
+ const initialPath = options.initialPath ?? '/';
463
+
464
+ const resolveRoute = (fullPath: string): TestRoute => {
465
+ const { path, query, hash } = parsePath(fullPath, base);
466
+ const { matched, params } = matchRoute(path, routes);
467
+ return { path, params, query, matched, hash };
468
+ };
469
+
470
+ const routeSignal = signal<TestRoute>(resolveRoute(initialPath));
471
+
472
+ return {
473
+ push(path: string): void {
474
+ routeSignal.value = resolveRoute(path);
475
+ },
476
+ replace(path: string): void {
477
+ routeSignal.value = resolveRoute(path);
478
+ },
479
+ get currentRoute(): Signal<TestRoute> {
480
+ return routeSignal;
481
+ },
482
+ get routes(): MockRouteDefinition[] {
483
+ return routes;
484
+ },
485
+ destroy(): void {
486
+ routeSignal.dispose();
487
+ },
488
+ };
489
+ }
490
+
491
+ // ============================================================================
492
+ // fireEvent
493
+ // ============================================================================
494
+
495
+ /**
496
+ * Dispatches a synthetic event on an element and flushes pending effects.
497
+ *
498
+ * By default the event bubbles, is cancelable, and is composed (crosses
499
+ * shadow DOM boundaries). Pass a `detail` option to create a `CustomEvent`.
500
+ *
501
+ * @param el - The target element
502
+ * @param eventName - The event type (e.g. 'click', 'input', 'my-event')
503
+ * @param options - Event configuration
504
+ * @returns `true` if the event was not cancelled
505
+ *
506
+ * @example
507
+ * ```ts
508
+ * import { fireEvent } from '@bquery/bquery/testing';
509
+ *
510
+ * const button = document.createElement('button');
511
+ * let clicked = false;
512
+ * button.addEventListener('click', () => { clicked = true; });
513
+ * fireEvent(button, 'click');
514
+ * expect(clicked).toBe(true);
515
+ * ```
516
+ */
517
+ export function fireEvent(el: Element, eventName: string, options: FireEventOptions = {}): boolean {
518
+ if (!el) {
519
+ throw new Error('bQuery testing: fireEvent requires a valid element');
520
+ }
521
+ if (!eventName) {
522
+ throw new Error('bQuery testing: fireEvent requires an event name');
523
+ }
524
+
525
+ const { bubbles = true, cancelable = true, composed = true, detail } = options;
526
+
527
+ let event: Event;
528
+ if (detail !== undefined) {
529
+ event = new CustomEvent(eventName, { bubbles, cancelable, composed, detail });
530
+ } else {
531
+ event = new Event(eventName, { bubbles, cancelable, composed });
532
+ }
533
+
534
+ const result = el.dispatchEvent(event);
535
+
536
+ // Flush any effects triggered by event handlers
537
+ flushEffects();
538
+
539
+ return result;
540
+ }
541
+
542
+ // ============================================================================
543
+ // waitFor
544
+ // ============================================================================
545
+
546
+ /**
547
+ * Waits for a predicate to return `true`, polling at a configurable interval.
548
+ *
549
+ * Useful for asserting conditions that depend on asynchronous operations,
550
+ * timers, or deferred reactive updates.
551
+ *
552
+ * @param predicate - A function that returns `true` when the condition is met
553
+ * @param options - Timeout and interval configuration
554
+ * @returns A promise that resolves when the predicate returns `true`
555
+ * @throws {Error} If the predicate does not return `true` within the timeout
556
+ *
557
+ * @example
558
+ * ```ts
559
+ * import { waitFor } from '@bquery/bquery/testing';
560
+ *
561
+ * await waitFor(() => document.querySelector('.loaded') !== null, {
562
+ * timeout: 2000,
563
+ * });
564
+ * ```
565
+ */
566
+ export async function waitFor(
567
+ predicate: () => boolean | Promise<boolean>,
568
+ options: WaitForOptions = {}
569
+ ): Promise<void> {
570
+ if (typeof predicate !== 'function') {
571
+ throw new Error('bQuery testing: waitFor requires a predicate function');
572
+ }
573
+
574
+ const { timeout = 1000, interval = 10 } = options;
575
+ const start = Date.now();
576
+
577
+ while (true) {
578
+ try {
579
+ const result = await predicate();
580
+ if (result) return;
581
+ } catch {
582
+ // Predicate threw — treat as not-yet-met and keep polling
583
+ }
584
+
585
+ if (Date.now() - start >= timeout) {
586
+ throw new Error(
587
+ `bQuery testing: waitFor timed out after ${timeout}ms — predicate never returned true`
588
+ );
589
+ }
590
+
591
+ await new Promise((resolve) => setTimeout(resolve, interval));
592
+ }
593
+ }