@praxisjs/devtools 0.2.4 → 0.2.5
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/CHANGELOG.md +12 -0
- package/assets/uno.generated.css +4 -1
- package/dist/index.js +1 -1
- package/package.json +6 -6
- package/src/__tests__/debug.test.ts +262 -0
- package/src/__tests__/event-bus.test.ts +76 -0
- package/src/__tests__/format-time.test.ts +52 -0
- package/src/__tests__/format-value.test.ts +46 -0
- package/src/__tests__/registry.test.ts +331 -0
- package/src/__tests__/trace.test.ts +149 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @praxisjs/devtools
|
|
2
2
|
|
|
3
|
+
## 0.2.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 00119ca: Regenerate UnoCSS bundle
|
|
8
|
+
- Updated dependencies [ea59035]
|
|
9
|
+
- Updated dependencies [d11a10a]
|
|
10
|
+
- @praxisjs/decorators@0.4.3
|
|
11
|
+
- @praxisjs/core@0.4.2
|
|
12
|
+
- @praxisjs/runtime@0.2.4
|
|
13
|
+
- @praxisjs/jsx@0.3.3
|
|
14
|
+
|
|
3
15
|
## 0.2.4
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/assets/uno.generated.css
CHANGED
|
@@ -65,12 +65,14 @@
|
|
|
65
65
|
.h-\[2px\]{height:2px;}
|
|
66
66
|
.h-\[32px\]{height:32px;}
|
|
67
67
|
.h-\[6px\]{height:6px;}
|
|
68
|
-
.h-1
|
|
68
|
+
.h-1,
|
|
69
|
+
.h1{height:0.25rem;}
|
|
69
70
|
.h-10{height:2.5rem;}
|
|
70
71
|
.h-3{height:0.75rem;}
|
|
71
72
|
.h-7{height:1.75rem;}
|
|
72
73
|
.h-full{height:100%;}
|
|
73
74
|
.h-screen{height:100vh;}
|
|
75
|
+
.h2{height:0.5rem;}
|
|
74
76
|
.max-w-\[200px\]{max-width:200px;}
|
|
75
77
|
.min-w-0{min-width:0;}
|
|
76
78
|
.w-\[13px\]{width:13px;}
|
|
@@ -108,6 +110,7 @@
|
|
|
108
110
|
.overflow-hidden{overflow:hidden;}
|
|
109
111
|
.overflow-y-auto{overflow-y:auto;}
|
|
110
112
|
.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
113
|
+
.b,
|
|
111
114
|
.border{border-width:1px;}
|
|
112
115
|
.border-b{border-bottom-width:1px;}
|
|
113
116
|
.border-l{border-left-width:1px;}
|
package/dist/index.js
CHANGED
|
@@ -26,7 +26,7 @@ import { StatelessComponent as y, StatefulComponent as J } from "@praxisjs/core"
|
|
|
26
26
|
import { Component as m, State as C, Prop as Z, Emit as Vt } from "@praxisjs/decorators";
|
|
27
27
|
import { render as Ge } from "@praxisjs/runtime";
|
|
28
28
|
import { computed as Pt } from "@praxisjs/core/internal";
|
|
29
|
-
const Jt = "data:image/svg+xml,%3csvg%20width='1024'%20height='1024'%20viewBox='0%200%201024%201024'%20fill='none'%20xmlns='http://www.w3.org/2000/svg'%3e%3cpath%20fill-rule='evenodd'%20clip-rule='evenodd'%20d='M605.411%20445.66L641.322%20498.293L641.342%20498.278L697.538%20578.57L742.673%20512.806L512.5%20178.878L281.327%20512.806L512.5%20848.182L603.519%20715.561L650.559%20782.134L512.701%20983L187%20510.478L512.701%2040L837%20510.478L698.206%20712.709L668.895%20671.104L604.033%20578.775L512%20712.504L373.199%20511.5L512.233%20309.092L584.13%20414.469L536.746%20480.456L513.69%20445.66L466.305%20511.5L512.166%20577.425L556.48%20513.91L605.411%20445.66Z'%20fill='url(%23paint0_linear_3024_20)'/%3e%3cdefs%3e%3clinearGradient%20id='paint0_linear_3024_20'%20x1='512'%20y1='40'%20x2='512'%20y2='983'%20gradientUnits='userSpaceOnUse'%3e%3cstop%20stop-color='%239B76FF'%20stop-opacity='0.86'/%3e%3cstop%20offset='1'%20stop-color='%232D246B'/%3e%3c/linearGradient%3e%3c/defs%3e%3c/svg%3e", en = '*,:before,:after{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgb(0 0 0 / 0);--un-ring-shadow:0 0 rgb(0 0 0 / 0);--un-shadow-inset: ;--un-shadow:0 0 rgb(0 0 0 / 0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgb(147 197 253 / .5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: }::backdrop{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgb(0 0 0 / 0);--un-ring-shadow:0 0 rgb(0 0 0 / 0);--un-shadow-inset: ;--un-shadow:0 0 rgb(0 0 0 / 0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgb(147 197 253 / .5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: }:host{all:initial;display:block;font-family:Inter,ui-sans-serif,system-ui,-apple-system,sans-serif}*{box-sizing:border-box}input::placeholder{color:#6f6a8f}input:focus{outline:none;box-shadow:0 0 0 2px #9b90e64d}::-webkit-scrollbar{width:4px;height:4px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:#9b90e633;border-radius:2px}::-webkit-scrollbar-thumb:hover{background:#9b90e666}.container{width:100%}@media(min-width:640px){.container{max-width:640px}}@media(min-width:768px){.container{max-width:768px}}@media(min-width:1024px){.container{max-width:1024px}}@media(min-width:1280px){.container{max-width:1280px}}@media(min-width:1536px){.container{max-width:1536px}}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.bottom-0{bottom:0}.bottom-5{bottom:1.25rem}.left-0{left:0}.right-0{right:0}.right-5{right:1.25rem}.top-0{top:0}.top-full{top:100%}.z-\\[2147483647\\]{z-index:2147483647}.z-10{z-index:10}.grid{display:grid}.grid-cols-\\[1\\.2fr_0\\.8fr_1fr_auto\\]{grid-template-columns:1.2fr .8fr 1fr auto}.m6{margin:1.5rem}.mx-1{margin-left:.25rem;margin-right:.25rem}.mr-\\[2px\\]{margin-right:2px}.ms{margin-inline-start:1rem}.mt-1{margin-top:.25rem}.box-border{box-sizing:border-box}.inline-block{display:inline-block}.h-\\[13px\\]{height:13px}.h-\\[22px\\]{height:22px}.h-\\[2px\\]{height:2px}.h-\\[32px\\]{height:32px}.h-\\[6px\\]{height:6px}.h-1{height:.25rem}.h-10{height:2.5rem}.h-3{height:.75rem}.h-7{height:1.75rem}.h-full{height:100%}.h-screen{height:100vh}.max-w-\\[200px\\]{max-width:200px}.min-w-0{min-width:0}.w-\\[13px\\]{width:13px}.w-\\[22px\\]{width:22px}.w-\\[2px\\]{width:2px}.w-\\[6px\\]{width:6px}.w-1{width:.25rem}.w-10{width:2.5rem}.w-12{width:3rem}.w-4{width:1rem}.w-full{width:100%}.w-px{width:1px}.flex{display:flex}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.flex-row-reverse{flex-direction:row-reverse}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-wrap{flex-wrap:wrap}.cursor-\\[ew-resize\\]{cursor:ew-resize}.cursor-\\[ns-resize\\]{cursor:ns-resize}.cursor-default{cursor:default}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;user-select:none}.items-center{align-items:center}.items-stretch{align-items:stretch}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-\\[2px\\]{gap:2px}.gap-\\[3px\\]{gap:3px}.gap-\\[6px\\]{gap:6px}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-border{--un-border-opacity:.25;border-color:rgba(155,144,230,var(--un-border-opacity))}.border-warn{--un-border-opacity:1;border-color:rgb(217 119 6 / var(--un-border-opacity))}.hover\\:border-accent:hover{--un-border-opacity:1;border-color:rgb(155 144 230 / var(--un-border-opacity))}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.rounded-r{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.bg-\\[rgba\\(124\\,109\\,214\\,0\\.14\\)\\]{--un-bg-opacity:.14;background-color:rgba(124,109,214,var(--un-bg-opacity))}.bg-\\[rgba\\(14\\,165\\,122\\,0\\.14\\)\\]{--un-bg-opacity:.14;background-color:rgba(14,165,122,var(--un-bg-opacity))}.bg-\\[rgba\\(155\\,144\\,230\\,0\\.14\\)\\]{--un-bg-opacity:.14;background-color:rgba(155,144,230,var(--un-bg-opacity))}.bg-\\[rgba\\(217\\,119\\,6\\,0\\.14\\)\\]{--un-bg-opacity:.14;background-color:rgba(217,119,6,var(--un-bg-opacity))}.bg-\\[rgba\\(220\\,38\\,38\\,0\\.14\\)\\]{--un-bg-opacity:.14;background-color:rgba(220,38,38,var(--un-bg-opacity))}.bg-\\[rgba\\(74\\,222\\,128\\,0\\.12\\)\\]{--un-bg-opacity:.12;background-color:rgba(74,222,128,var(--un-bg-opacity))}.bg-accent{--un-bg-opacity:1;background-color:rgb(155 144 230 / var(--un-bg-opacity))}.bg-bg{--un-bg-opacity:1;background-color:rgb(15 13 23 / var(--un-bg-opacity))}.bg-border{--un-bg-opacity:.25;background-color:rgba(155,144,230,var(--un-bg-opacity))}.bg-header{--un-bg-opacity:1;background-color:rgb(22 19 38 / var(--un-bg-opacity))}.bg-input,.bg-section{--un-bg-opacity:1;background-color:rgb(27 24 48 / var(--un-bg-opacity))}.bg-selected{--un-bg-opacity:1;background-color:rgb(34 30 61 / var(--un-bg-opacity))}.bg-soft{--un-bg-opacity:.18;background-color:rgba(155,144,230,var(--un-bg-opacity))}.bg-subtle{--un-bg-opacity:1;background-color:rgb(111 106 143 / var(--un-bg-opacity))}.bg-success{--un-bg-opacity:1;background-color:rgb(14 165 122 / var(--un-bg-opacity))}.bg-transparent{background-color:transparent}.hover\\:bg-accent:hover{--un-bg-opacity:1;background-color:rgb(155 144 230 / var(--un-bg-opacity))}.hover\\:bg-section:hover{--un-bg-opacity:1;background-color:rgb(27 24 48 / var(--un-bg-opacity))}.hover\\:bg-soft:hover{--un-bg-opacity:.18;background-color:rgba(155,144,230,var(--un-bg-opacity))}.p-1{padding:.25rem}.px{padding-left:1rem;padding-right:1rem}.px-\\[6px\\]{padding-left:6px;padding-right:6px}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.py-\\[2px\\]{padding-top:2px;padding-bottom:2px}.py-\\[3px\\]{padding-top:3px;padding-bottom:3px}.py-\\[4px\\]{padding-top:4px;padding-bottom:4px}.py-\\[5px\\]{padding-top:5px;padding-bottom:5px}.py-12{padding-top:3rem;padding-bottom:3rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.pl-\\[10px\\]{padding-left:10px}.pl-\\[56px\\]{padding-left:56px}.pl-1{padding-left:.25rem}.pr-\\[14px\\]{padding-right:14px}.pr-3{padding-right:.75rem}.text-center{text-align:center}.text-right{text-align:right}.text-\\[10px\\]{font-size:10px}.text-\\[11px\\]{font-size:11px}.text-\\[12px\\]{font-size:12px}.text-\\[20px\\]{font-size:20px}.text-\\[9px\\]{font-size:9px}.text-\\[\\#0ea57a\\]{--un-text-opacity:1;color:rgb(14 165 122 / var(--un-text-opacity))}.text-\\[\\#4ade80\\]{--un-text-opacity:1;color:rgb(74 222 128 / var(--un-text-opacity))}.text-\\[\\#7c6dd6\\]{--un-text-opacity:1;color:rgb(124 109 214 / var(--un-text-opacity))}.text-\\[\\#9b90e6\\],.text-accent{--un-text-opacity:1;color:rgb(155 144 230 / var(--un-text-opacity))}.text-\\[\\#d97706\\],.text-warn{--un-text-opacity:1;color:rgb(217 119 6 / var(--un-text-opacity))}.text-\\[\\#dc2626\\]{--un-text-opacity:1;color:rgb(220 38 38 / var(--un-text-opacity))}.text-muted{--un-text-opacity:1;color:rgb(199 195 230 / var(--un-text-opacity))}.text-subtle{--un-text-opacity:1;color:rgb(111 106 143 / var(--un-text-opacity))}.text-text,.hover\\:text-text:hover{--un-text-opacity:1;color:rgb(244 242 255 / var(--un-text-opacity))}.color-white{--un-text-opacity:1;color:rgb(255 255 255 / var(--un-text-opacity))}.hover\\:color-muted:hover{--un-text-opacity:1;color:rgb(199 195 230 / var(--un-text-opacity))}.font-bold{font-weight:700}.font-semibold{font-weight:600}.leading-none{line-height:1}.leading-relaxed{line-height:1.625}.tracking-\\[0\\.07em\\]{letter-spacing:.07em}.tracking-\\[0\\.12em\\]{letter-spacing:.12em}.tracking-widest{letter-spacing:.1em}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.font-sans{font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji"}.uppercase{text-transform:uppercase}.tabular-nums{--un-numeric-spacing:tabular-nums;font-variant-numeric:var(--un-ordinal) var(--un-slashed-zero) var(--un-numeric-figure) var(--un-numeric-spacing) var(--un-numeric-fraction)}.shadow{--un-shadow:var(--un-shadow-inset) 0 1px 3px 0 var(--un-shadow-color, rgb(0 0 0 / .1)),var(--un-shadow-inset) 0 1px 2px -1px var(--un-shadow-color, rgb(0 0 0 / .1));box-shadow:var(--un-ring-offset-shadow),var(--un-ring-shadow),var(--un-shadow)}.shadow-\\[0_8px_32px_rgba\\(0\\,0\\,0\\,0\\.7\\)\\]{--un-shadow:0 8px 32px var(--un-shadow-color, rgba(0, 0, 0, .7));box-shadow:var(--un-ring-offset-shadow),var(--un-ring-shadow),var(--un-shadow)}.shadow-lg{--un-shadow:var(--un-shadow-inset) 0 10px 15px -3px var(--un-shadow-color, rgb(0 0 0 / .1)),var(--un-shadow-inset) 0 4px 6px -4px var(--un-shadow-color, rgb(0 0 0 / .1));box-shadow:var(--un-ring-offset-shadow),var(--un-ring-shadow),var(--un-shadow)}.hover\\:shadow-\\[0_8px_32px_rgba\\(56\\,189\\,248\\,0\\.1\\)\\]:hover{--un-shadow:0 8px 32px var(--un-shadow-color, rgba(56, 189, 248, .1));box-shadow:var(--un-ring-offset-shadow),var(--un-ring-shadow),var(--un-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.filter{filter:var(--un-blur) var(--un-brightness) var(--un-contrast) var(--un-drop-shadow) var(--un-grayscale) var(--un-hue-rotate) var(--un-invert) var(--un-saturate) var(--un-sepia)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-shadow{transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-100{transition-duration:.1s}.duration-150{transition-duration:.15s}.duration-200{transition-duration:.2s}';
|
|
29
|
+
const Jt = "data:image/svg+xml,%3csvg%20width='1024'%20height='1024'%20viewBox='0%200%201024%201024'%20fill='none'%20xmlns='http://www.w3.org/2000/svg'%3e%3cpath%20fill-rule='evenodd'%20clip-rule='evenodd'%20d='M605.411%20445.66L641.322%20498.293L641.342%20498.278L697.538%20578.57L742.673%20512.806L512.5%20178.878L281.327%20512.806L512.5%20848.182L603.519%20715.561L650.559%20782.134L512.701%20983L187%20510.478L512.701%2040L837%20510.478L698.206%20712.709L668.895%20671.104L604.033%20578.775L512%20712.504L373.199%20511.5L512.233%20309.092L584.13%20414.469L536.746%20480.456L513.69%20445.66L466.305%20511.5L512.166%20577.425L556.48%20513.91L605.411%20445.66Z'%20fill='url(%23paint0_linear_3024_20)'/%3e%3cdefs%3e%3clinearGradient%20id='paint0_linear_3024_20'%20x1='512'%20y1='40'%20x2='512'%20y2='983'%20gradientUnits='userSpaceOnUse'%3e%3cstop%20stop-color='%239B76FF'%20stop-opacity='0.86'/%3e%3cstop%20offset='1'%20stop-color='%232D246B'/%3e%3c/linearGradient%3e%3c/defs%3e%3c/svg%3e", en = '*,:before,:after{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgb(0 0 0 / 0);--un-ring-shadow:0 0 rgb(0 0 0 / 0);--un-shadow-inset: ;--un-shadow:0 0 rgb(0 0 0 / 0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgb(147 197 253 / .5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: }::backdrop{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgb(0 0 0 / 0);--un-ring-shadow:0 0 rgb(0 0 0 / 0);--un-shadow-inset: ;--un-shadow:0 0 rgb(0 0 0 / 0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgb(147 197 253 / .5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: }:host{all:initial;display:block;font-family:Inter,ui-sans-serif,system-ui,-apple-system,sans-serif}*{box-sizing:border-box}input::placeholder{color:#6f6a8f}input:focus{outline:none;box-shadow:0 0 0 2px #9b90e64d}::-webkit-scrollbar{width:4px;height:4px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:#9b90e633;border-radius:2px}::-webkit-scrollbar-thumb:hover{background:#9b90e666}.container{width:100%}@media(min-width:640px){.container{max-width:640px}}@media(min-width:768px){.container{max-width:768px}}@media(min-width:1024px){.container{max-width:1024px}}@media(min-width:1280px){.container{max-width:1280px}}@media(min-width:1536px){.container{max-width:1536px}}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.bottom-0{bottom:0}.bottom-5{bottom:1.25rem}.left-0{left:0}.right-0{right:0}.right-5{right:1.25rem}.top-0{top:0}.top-full{top:100%}.z-\\[2147483647\\]{z-index:2147483647}.z-10{z-index:10}.grid{display:grid}.grid-cols-\\[1\\.2fr_0\\.8fr_1fr_auto\\]{grid-template-columns:1.2fr .8fr 1fr auto}.m6{margin:1.5rem}.mx-1{margin-left:.25rem;margin-right:.25rem}.mr-\\[2px\\]{margin-right:2px}.ms{margin-inline-start:1rem}.mt-1{margin-top:.25rem}.box-border{box-sizing:border-box}.inline-block{display:inline-block}.h-\\[13px\\]{height:13px}.h-\\[22px\\]{height:22px}.h-\\[2px\\]{height:2px}.h-\\[32px\\]{height:32px}.h-\\[6px\\]{height:6px}.h-1,.h1{height:.25rem}.h-10{height:2.5rem}.h-3{height:.75rem}.h-7{height:1.75rem}.h-full{height:100%}.h-screen{height:100vh}.h2{height:.5rem}.max-w-\\[200px\\]{max-width:200px}.min-w-0{min-width:0}.w-\\[13px\\]{width:13px}.w-\\[22px\\]{width:22px}.w-\\[2px\\]{width:2px}.w-\\[6px\\]{width:6px}.w-1{width:.25rem}.w-10{width:2.5rem}.w-12{width:3rem}.w-4{width:1rem}.w-full{width:100%}.w-px{width:1px}.flex{display:flex}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.flex-row-reverse{flex-direction:row-reverse}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-wrap{flex-wrap:wrap}.cursor-\\[ew-resize\\]{cursor:ew-resize}.cursor-\\[ns-resize\\]{cursor:ns-resize}.cursor-default{cursor:default}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;user-select:none}.items-center{align-items:center}.items-stretch{align-items:stretch}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-\\[2px\\]{gap:2px}.gap-\\[3px\\]{gap:3px}.gap-\\[6px\\]{gap:6px}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.b,.border{border-width:1px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-border{--un-border-opacity:.25;border-color:rgba(155,144,230,var(--un-border-opacity))}.border-warn{--un-border-opacity:1;border-color:rgb(217 119 6 / var(--un-border-opacity))}.hover\\:border-accent:hover{--un-border-opacity:1;border-color:rgb(155 144 230 / var(--un-border-opacity))}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.rounded-r{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.bg-\\[rgba\\(124\\,109\\,214\\,0\\.14\\)\\]{--un-bg-opacity:.14;background-color:rgba(124,109,214,var(--un-bg-opacity))}.bg-\\[rgba\\(14\\,165\\,122\\,0\\.14\\)\\]{--un-bg-opacity:.14;background-color:rgba(14,165,122,var(--un-bg-opacity))}.bg-\\[rgba\\(155\\,144\\,230\\,0\\.14\\)\\]{--un-bg-opacity:.14;background-color:rgba(155,144,230,var(--un-bg-opacity))}.bg-\\[rgba\\(217\\,119\\,6\\,0\\.14\\)\\]{--un-bg-opacity:.14;background-color:rgba(217,119,6,var(--un-bg-opacity))}.bg-\\[rgba\\(220\\,38\\,38\\,0\\.14\\)\\]{--un-bg-opacity:.14;background-color:rgba(220,38,38,var(--un-bg-opacity))}.bg-\\[rgba\\(74\\,222\\,128\\,0\\.12\\)\\]{--un-bg-opacity:.12;background-color:rgba(74,222,128,var(--un-bg-opacity))}.bg-accent{--un-bg-opacity:1;background-color:rgb(155 144 230 / var(--un-bg-opacity))}.bg-bg{--un-bg-opacity:1;background-color:rgb(15 13 23 / var(--un-bg-opacity))}.bg-border{--un-bg-opacity:.25;background-color:rgba(155,144,230,var(--un-bg-opacity))}.bg-header{--un-bg-opacity:1;background-color:rgb(22 19 38 / var(--un-bg-opacity))}.bg-input,.bg-section{--un-bg-opacity:1;background-color:rgb(27 24 48 / var(--un-bg-opacity))}.bg-selected{--un-bg-opacity:1;background-color:rgb(34 30 61 / var(--un-bg-opacity))}.bg-soft{--un-bg-opacity:.18;background-color:rgba(155,144,230,var(--un-bg-opacity))}.bg-subtle{--un-bg-opacity:1;background-color:rgb(111 106 143 / var(--un-bg-opacity))}.bg-success{--un-bg-opacity:1;background-color:rgb(14 165 122 / var(--un-bg-opacity))}.bg-transparent{background-color:transparent}.hover\\:bg-accent:hover{--un-bg-opacity:1;background-color:rgb(155 144 230 / var(--un-bg-opacity))}.hover\\:bg-section:hover{--un-bg-opacity:1;background-color:rgb(27 24 48 / var(--un-bg-opacity))}.hover\\:bg-soft:hover{--un-bg-opacity:.18;background-color:rgba(155,144,230,var(--un-bg-opacity))}.p-1{padding:.25rem}.px{padding-left:1rem;padding-right:1rem}.px-\\[6px\\]{padding-left:6px;padding-right:6px}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.py-\\[2px\\]{padding-top:2px;padding-bottom:2px}.py-\\[3px\\]{padding-top:3px;padding-bottom:3px}.py-\\[4px\\]{padding-top:4px;padding-bottom:4px}.py-\\[5px\\]{padding-top:5px;padding-bottom:5px}.py-12{padding-top:3rem;padding-bottom:3rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.pl-\\[10px\\]{padding-left:10px}.pl-\\[56px\\]{padding-left:56px}.pl-1{padding-left:.25rem}.pr-\\[14px\\]{padding-right:14px}.pr-3{padding-right:.75rem}.text-center{text-align:center}.text-right{text-align:right}.text-\\[10px\\]{font-size:10px}.text-\\[11px\\]{font-size:11px}.text-\\[12px\\]{font-size:12px}.text-\\[20px\\]{font-size:20px}.text-\\[9px\\]{font-size:9px}.text-\\[\\#0ea57a\\]{--un-text-opacity:1;color:rgb(14 165 122 / var(--un-text-opacity))}.text-\\[\\#4ade80\\]{--un-text-opacity:1;color:rgb(74 222 128 / var(--un-text-opacity))}.text-\\[\\#7c6dd6\\]{--un-text-opacity:1;color:rgb(124 109 214 / var(--un-text-opacity))}.text-\\[\\#9b90e6\\],.text-accent{--un-text-opacity:1;color:rgb(155 144 230 / var(--un-text-opacity))}.text-\\[\\#d97706\\],.text-warn{--un-text-opacity:1;color:rgb(217 119 6 / var(--un-text-opacity))}.text-\\[\\#dc2626\\]{--un-text-opacity:1;color:rgb(220 38 38 / var(--un-text-opacity))}.text-muted{--un-text-opacity:1;color:rgb(199 195 230 / var(--un-text-opacity))}.text-subtle{--un-text-opacity:1;color:rgb(111 106 143 / var(--un-text-opacity))}.text-text,.hover\\:text-text:hover{--un-text-opacity:1;color:rgb(244 242 255 / var(--un-text-opacity))}.color-white{--un-text-opacity:1;color:rgb(255 255 255 / var(--un-text-opacity))}.hover\\:color-muted:hover{--un-text-opacity:1;color:rgb(199 195 230 / var(--un-text-opacity))}.font-bold{font-weight:700}.font-semibold{font-weight:600}.leading-none{line-height:1}.leading-relaxed{line-height:1.625}.tracking-\\[0\\.07em\\]{letter-spacing:.07em}.tracking-\\[0\\.12em\\]{letter-spacing:.12em}.tracking-widest{letter-spacing:.1em}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.font-sans{font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji"}.uppercase{text-transform:uppercase}.tabular-nums{--un-numeric-spacing:tabular-nums;font-variant-numeric:var(--un-ordinal) var(--un-slashed-zero) var(--un-numeric-figure) var(--un-numeric-spacing) var(--un-numeric-fraction)}.shadow{--un-shadow:var(--un-shadow-inset) 0 1px 3px 0 var(--un-shadow-color, rgb(0 0 0 / .1)),var(--un-shadow-inset) 0 1px 2px -1px var(--un-shadow-color, rgb(0 0 0 / .1));box-shadow:var(--un-ring-offset-shadow),var(--un-ring-shadow),var(--un-shadow)}.shadow-\\[0_8px_32px_rgba\\(0\\,0\\,0\\,0\\.7\\)\\]{--un-shadow:0 8px 32px var(--un-shadow-color, rgba(0, 0, 0, .7));box-shadow:var(--un-ring-offset-shadow),var(--un-ring-shadow),var(--un-shadow)}.shadow-lg{--un-shadow:var(--un-shadow-inset) 0 10px 15px -3px var(--un-shadow-color, rgb(0 0 0 / .1)),var(--un-shadow-inset) 0 4px 6px -4px var(--un-shadow-color, rgb(0 0 0 / .1));box-shadow:var(--un-ring-offset-shadow),var(--un-ring-shadow),var(--un-shadow)}.hover\\:shadow-\\[0_8px_32px_rgba\\(56\\,189\\,248\\,0\\.1\\)\\]:hover{--un-shadow:0 8px 32px var(--un-shadow-color, rgba(56, 189, 248, .1));box-shadow:var(--un-ring-offset-shadow),var(--un-ring-shadow),var(--un-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.filter{filter:var(--un-blur) var(--un-brightness) var(--un-contrast) var(--un-drop-shadow) var(--un-grayscale) var(--un-hue-rotate) var(--un-invert) var(--un-saturate) var(--un-sepia)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-shadow{transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-100{transition-duration:.1s}.duration-150{transition-duration:.15s}.duration-200{transition-duration:.2s}';
|
|
30
30
|
class tn {
|
|
31
31
|
handlers = /* @__PURE__ */ new Map();
|
|
32
32
|
on(t, e) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@praxisjs/devtools",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@unocss/reset": "^66.6.2",
|
|
15
|
-
"@praxisjs/core": "0.4.
|
|
16
|
-
"@praxisjs/decorators": "0.4.
|
|
17
|
-
"@praxisjs/jsx": "0.3.
|
|
18
|
-
"@praxisjs/
|
|
19
|
-
"@praxisjs/
|
|
15
|
+
"@praxisjs/core": "0.4.2",
|
|
16
|
+
"@praxisjs/decorators": "0.4.3",
|
|
17
|
+
"@praxisjs/jsx": "0.3.3",
|
|
18
|
+
"@praxisjs/shared": "0.2.0",
|
|
19
|
+
"@praxisjs/runtime": "0.2.4"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@types/node": "^25.3.0",
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { beforeEach, describe, it, expect, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { computed, signal } from "@praxisjs/core/internal";
|
|
4
|
+
|
|
5
|
+
import { Registry } from "../core/registry";
|
|
6
|
+
import { Debug } from "../decorators/debug";
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
(Registry as unknown as { _instance: Registry | null })._instance = null;
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
// ── Method decorator ──────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
describe("Debug — method decorator", () => {
|
|
15
|
+
it("records a method:call timeline entry", () => {
|
|
16
|
+
const ctx = { kind: "method" as const, name: "doWork" } as ClassMethodDecoratorContext;
|
|
17
|
+
const original = vi.fn((_x: unknown) => "result");
|
|
18
|
+
|
|
19
|
+
const wrapped = Debug()(original as never, ctx) as (...args: unknown[]) => unknown;
|
|
20
|
+
|
|
21
|
+
class Comp { constructor() {} }
|
|
22
|
+
const instance = new Comp();
|
|
23
|
+
const registry = Registry.instance;
|
|
24
|
+
registry.registerComponent(instance, "Comp");
|
|
25
|
+
|
|
26
|
+
wrapped.call(instance, 42);
|
|
27
|
+
|
|
28
|
+
const entry = registry.getTimeline().find((t) => t.type === "method:call");
|
|
29
|
+
expect(entry).toBeDefined();
|
|
30
|
+
expect(entry?.label).toContain("doWork");
|
|
31
|
+
expect(entry?.data.args).toEqual([42]);
|
|
32
|
+
expect(entry?.data.result).toBe("result");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("records throw result when method throws", () => {
|
|
36
|
+
const ctx = { kind: "method" as const, name: "boom" } as ClassMethodDecoratorContext;
|
|
37
|
+
const original = () => { throw new Error("fail"); };
|
|
38
|
+
|
|
39
|
+
const wrapped = Debug()(original as never, ctx) as () => void;
|
|
40
|
+
|
|
41
|
+
class Comp {}
|
|
42
|
+
const instance = new Comp();
|
|
43
|
+
Registry.instance.registerComponent(instance, "Comp");
|
|
44
|
+
|
|
45
|
+
expect(() => wrapped.call(instance)).toThrow("fail");
|
|
46
|
+
|
|
47
|
+
const entry = Registry.instance.getTimeline().find((t) => t.type === "method:call");
|
|
48
|
+
expect(entry?.data.result).toContain("throw");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("uses custom label from options", () => {
|
|
52
|
+
const ctx = { kind: "method" as const, name: "internal" } as ClassMethodDecoratorContext;
|
|
53
|
+
const original = vi.fn(() => undefined);
|
|
54
|
+
|
|
55
|
+
const wrapped = Debug({ label: "myLabel" })(original as never, ctx) as () => void;
|
|
56
|
+
|
|
57
|
+
class Comp {}
|
|
58
|
+
const instance = new Comp();
|
|
59
|
+
Registry.instance.registerComponent(instance, "Comp");
|
|
60
|
+
wrapped.call(instance);
|
|
61
|
+
|
|
62
|
+
const entry = Registry.instance.getTimeline().find((t) => t.type === "method:call");
|
|
63
|
+
expect(entry?.label).toContain("myLabel");
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// ── Field decorator (wrapping @State getter/setter) ───────────────────────────
|
|
68
|
+
|
|
69
|
+
describe("Debug — field decorator wrapping @State", () => {
|
|
70
|
+
it("registers signal and tracks updates", () => {
|
|
71
|
+
const registry = Registry.instance;
|
|
72
|
+
const instance = { count: 0 } as Record<string, unknown>;
|
|
73
|
+
|
|
74
|
+
// Simulate what @State() does: install a getter/setter on the instance
|
|
75
|
+
const s = signal(0);
|
|
76
|
+
Object.defineProperty(instance, "count", {
|
|
77
|
+
get() { return s(); },
|
|
78
|
+
set(v: number) { s.set(v); },
|
|
79
|
+
enumerable: true,
|
|
80
|
+
configurable: true,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const initializers: Array<(this: unknown) => void> = [];
|
|
84
|
+
const ctx = {
|
|
85
|
+
kind: "field" as const,
|
|
86
|
+
name: "count",
|
|
87
|
+
addInitializer(fn: (this: unknown) => void) { initializers.push(fn); },
|
|
88
|
+
} as ClassFieldDecoratorContext;
|
|
89
|
+
|
|
90
|
+
Debug()(undefined, ctx);
|
|
91
|
+
initializers.forEach((fn) => fn.call(instance));
|
|
92
|
+
|
|
93
|
+
const signals = registry.getSignals();
|
|
94
|
+
expect(signals.length).toBeGreaterThan(0);
|
|
95
|
+
expect(signals[0].label).toBe("count");
|
|
96
|
+
expect(signals[0].value).toBe(0);
|
|
97
|
+
|
|
98
|
+
// Read the property through the getter (covers the get() function body)
|
|
99
|
+
const readValue = (instance as Record<string, unknown>).count;
|
|
100
|
+
expect(readValue).toBe(0);
|
|
101
|
+
|
|
102
|
+
// Trigger update through the wrapped setter
|
|
103
|
+
(instance as Record<string, unknown>).count = 10;
|
|
104
|
+
|
|
105
|
+
// The set through the defined property calls updateSignal
|
|
106
|
+
expect(registry.getSignals()[0].value).toBe(10);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// ── Field decorator (computed field) ─────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
describe("Debug — field decorator with computed value", () => {
|
|
113
|
+
it("registers computed signal and tracks updates", () => {
|
|
114
|
+
const registry = Registry.instance;
|
|
115
|
+
const src = signal(1);
|
|
116
|
+
const c = computed(() => src() * 2);
|
|
117
|
+
|
|
118
|
+
const instance = { doubled: c } as Record<string, unknown>;
|
|
119
|
+
|
|
120
|
+
const initializers: Array<(this: unknown) => void> = [];
|
|
121
|
+
const ctx = {
|
|
122
|
+
kind: "field" as const,
|
|
123
|
+
name: "doubled",
|
|
124
|
+
addInitializer(fn: (this: unknown) => void) { initializers.push(fn); },
|
|
125
|
+
} as ClassFieldDecoratorContext;
|
|
126
|
+
|
|
127
|
+
Debug()(undefined, ctx);
|
|
128
|
+
initializers.forEach((fn) => fn.call(instance));
|
|
129
|
+
|
|
130
|
+
const signals = registry.getSignals();
|
|
131
|
+
expect(signals.length).toBeGreaterThan(0);
|
|
132
|
+
expect(signals[0].label).toBe("doubled");
|
|
133
|
+
expect(signals[0].value).toBe(2);
|
|
134
|
+
|
|
135
|
+
src.set(5);
|
|
136
|
+
expect(registry.getSignals()[0].value).toBe(10);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("warns when field value is not a computed and not undefined", () => {
|
|
140
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
141
|
+
const instance = { myField: "not-computed" } as Record<string, unknown>;
|
|
142
|
+
|
|
143
|
+
const initializers: Array<(this: unknown) => void> = [];
|
|
144
|
+
const ctx = {
|
|
145
|
+
kind: "field" as const,
|
|
146
|
+
name: "myField",
|
|
147
|
+
addInitializer(fn: (this: unknown) => void) { initializers.push(fn); },
|
|
148
|
+
} as ClassFieldDecoratorContext;
|
|
149
|
+
|
|
150
|
+
Debug()(undefined, ctx);
|
|
151
|
+
initializers.forEach((fn) => fn.call(instance));
|
|
152
|
+
|
|
153
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining("[PraxisJS DevTools]"));
|
|
154
|
+
warn.mockRestore();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("warns when computed field is replaced with non-computed value via setter", () => {
|
|
158
|
+
const registry = Registry.instance;
|
|
159
|
+
const src = signal(1);
|
|
160
|
+
const c = computed(() => src() * 2);
|
|
161
|
+
|
|
162
|
+
const instance = { doubled: c } as Record<string, unknown>;
|
|
163
|
+
|
|
164
|
+
const initializers: Array<(this: unknown) => void> = [];
|
|
165
|
+
const ctx = {
|
|
166
|
+
kind: "field" as const,
|
|
167
|
+
name: "doubled",
|
|
168
|
+
addInitializer(fn: (this: unknown) => void) { initializers.push(fn); },
|
|
169
|
+
} as ClassFieldDecoratorContext;
|
|
170
|
+
|
|
171
|
+
Debug()(undefined, ctx);
|
|
172
|
+
initializers.forEach((fn) => fn.call(instance));
|
|
173
|
+
|
|
174
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
175
|
+
// Set a non-computed value through the property setter
|
|
176
|
+
(instance as Record<string, unknown>).doubled = "not-a-computed";
|
|
177
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining("[PraxisJS DevTools]"));
|
|
178
|
+
warn.mockRestore();
|
|
179
|
+
|
|
180
|
+
void registry;
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("updates slot when computed field is replaced with a new computed", () => {
|
|
184
|
+
const registry = Registry.instance;
|
|
185
|
+
const src = signal(1);
|
|
186
|
+
const c1 = computed(() => src() * 2);
|
|
187
|
+
const c2 = computed(() => src() * 10);
|
|
188
|
+
|
|
189
|
+
const instance = { doubled: c1 } as Record<string, unknown>;
|
|
190
|
+
|
|
191
|
+
const initializers: Array<(this: unknown) => void> = [];
|
|
192
|
+
const ctx = {
|
|
193
|
+
kind: "field" as const,
|
|
194
|
+
name: "doubled",
|
|
195
|
+
addInitializer(fn: (this: unknown) => void) { initializers.push(fn); },
|
|
196
|
+
} as ClassFieldDecoratorContext;
|
|
197
|
+
|
|
198
|
+
Debug()(undefined, ctx);
|
|
199
|
+
initializers.forEach((fn) => fn.call(instance));
|
|
200
|
+
|
|
201
|
+
// Replace with new computed
|
|
202
|
+
(instance as Record<string, unknown>).doubled = c2;
|
|
203
|
+
|
|
204
|
+
// The getter should now return c2
|
|
205
|
+
const newComputed = (instance as Record<string, unknown>).doubled;
|
|
206
|
+
expect(typeof newComputed).toBe("function");
|
|
207
|
+
|
|
208
|
+
void registry;
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("does not warn when field value is undefined", () => {
|
|
212
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
213
|
+
const instance = { myUndef: undefined } as Record<string, unknown>;
|
|
214
|
+
|
|
215
|
+
const initializers: Array<(this: unknown) => void> = [];
|
|
216
|
+
const ctx = {
|
|
217
|
+
kind: "field" as const,
|
|
218
|
+
name: "myUndef",
|
|
219
|
+
addInitializer(fn: (this: unknown) => void) { initializers.push(fn); },
|
|
220
|
+
} as ClassFieldDecoratorContext;
|
|
221
|
+
|
|
222
|
+
Debug()(undefined, ctx);
|
|
223
|
+
initializers.forEach((fn) => fn.call(instance));
|
|
224
|
+
|
|
225
|
+
expect(warn).not.toHaveBeenCalled();
|
|
226
|
+
warn.mockRestore();
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// ── Getter decorator ──────────────────────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
describe("Debug — getter decorator", () => {
|
|
233
|
+
it("returns undefined (void) for getter kind", async () => {
|
|
234
|
+
const initializers: Array<(this: unknown) => void> = [];
|
|
235
|
+
const ctx = {
|
|
236
|
+
kind: "getter" as const,
|
|
237
|
+
name: "myGetter",
|
|
238
|
+
addInitializer(fn: (this: unknown) => void) { initializers.push(fn); },
|
|
239
|
+
} as ClassGetterDecoratorContext;
|
|
240
|
+
|
|
241
|
+
const src = signal(3);
|
|
242
|
+
const result = Debug()(() => src() * 2 as unknown as never, ctx);
|
|
243
|
+
expect(result).toBeUndefined();
|
|
244
|
+
|
|
245
|
+
// Run the initializer on a fake instance
|
|
246
|
+
const instance = { myGetter: computed(() => src() * 2) };
|
|
247
|
+
initializers.forEach((fn) => fn.call(instance));
|
|
248
|
+
|
|
249
|
+
// Wait for queueMicrotask
|
|
250
|
+
await new Promise<void>((r) => setTimeout(r, 0));
|
|
251
|
+
|
|
252
|
+
const signals = Registry.instance.getSignals();
|
|
253
|
+
expect(signals.length).toBeGreaterThan(0);
|
|
254
|
+
|
|
255
|
+
// Update src to trigger the subscribe callback (covers the updateSignal path)
|
|
256
|
+
const signalsBefore = Registry.instance.getSignals()[0]?.value;
|
|
257
|
+
src.set(10);
|
|
258
|
+
// The computed updates: src() * 2 = 20
|
|
259
|
+
await new Promise<void>((r) => setTimeout(r, 0));
|
|
260
|
+
expect(signalsBefore).toBe(6); // 3 * 2 = 6
|
|
261
|
+
});
|
|
262
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { EventBus } from "../core/event-bus";
|
|
4
|
+
|
|
5
|
+
describe("EventBus", () => {
|
|
6
|
+
it("calls registered handler when event is emitted", () => {
|
|
7
|
+
const bus = new EventBus();
|
|
8
|
+
const handler = vi.fn();
|
|
9
|
+
bus.on("test", handler);
|
|
10
|
+
bus.emit("test", "payload");
|
|
11
|
+
expect(handler).toHaveBeenCalledWith("payload");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("calls multiple handlers for the same event", () => {
|
|
15
|
+
const bus = new EventBus();
|
|
16
|
+
const h1 = vi.fn();
|
|
17
|
+
const h2 = vi.fn();
|
|
18
|
+
bus.on("event", h1);
|
|
19
|
+
bus.on("event", h2);
|
|
20
|
+
bus.emit("event", 42);
|
|
21
|
+
expect(h1).toHaveBeenCalledWith(42);
|
|
22
|
+
expect(h2).toHaveBeenCalledWith(42);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("does not call handlers for different events", () => {
|
|
26
|
+
const bus = new EventBus();
|
|
27
|
+
const handler = vi.fn();
|
|
28
|
+
bus.on("a", handler);
|
|
29
|
+
bus.emit("b", "data");
|
|
30
|
+
expect(handler).not.toHaveBeenCalled();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("does not throw when emitting to an event with no handlers", () => {
|
|
34
|
+
const bus = new EventBus();
|
|
35
|
+
expect(() => bus.emit("no-listeners", null)).not.toThrow();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("unsubscribe function removes the handler", () => {
|
|
39
|
+
const bus = new EventBus();
|
|
40
|
+
const handler = vi.fn();
|
|
41
|
+
const unsub = bus.on("event", handler);
|
|
42
|
+
unsub();
|
|
43
|
+
bus.emit("event", "data");
|
|
44
|
+
expect(handler).not.toHaveBeenCalled();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("unsubscribing one handler does not affect others", () => {
|
|
48
|
+
const bus = new EventBus();
|
|
49
|
+
const h1 = vi.fn();
|
|
50
|
+
const h2 = vi.fn();
|
|
51
|
+
const unsub = bus.on("event", h1);
|
|
52
|
+
bus.on("event", h2);
|
|
53
|
+
unsub();
|
|
54
|
+
bus.emit("event", "data");
|
|
55
|
+
expect(h1).not.toHaveBeenCalled();
|
|
56
|
+
expect(h2).toHaveBeenCalledWith("data");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("passes payload correctly to handler", () => {
|
|
60
|
+
const bus = new EventBus();
|
|
61
|
+
const handler = vi.fn();
|
|
62
|
+
const payload = { key: "value", nested: { n: 1 } };
|
|
63
|
+
bus.on("event", handler);
|
|
64
|
+
bus.emit("event", payload);
|
|
65
|
+
expect(handler).toHaveBeenCalledWith(payload);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("each EventBus instance is independent", () => {
|
|
69
|
+
const bus1 = new EventBus();
|
|
70
|
+
const bus2 = new EventBus();
|
|
71
|
+
const handler = vi.fn();
|
|
72
|
+
bus1.on("event", handler);
|
|
73
|
+
bus2.emit("event", "data");
|
|
74
|
+
expect(handler).not.toHaveBeenCalled();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { beforeEach, afterEach, describe, it, expect, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { time } from "../utils/format-time";
|
|
4
|
+
|
|
5
|
+
describe("time", () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
vi.useFakeTimers();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
vi.useRealTimers();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("formats diff < 1000ms as milliseconds", () => {
|
|
15
|
+
// Date.now() = 1000, ts = 1000 - diff
|
|
16
|
+
vi.setSystemTime(1000);
|
|
17
|
+
expect(time(1000 - 500)).toBe("500ms"); // diff = 500
|
|
18
|
+
expect(time(1000 - 999)).toBe("999ms"); // diff = 999
|
|
19
|
+
expect(time(1000)).toBe("0ms"); // diff = 0
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("formats diff >= 1000ms and < 60000ms as seconds", () => {
|
|
23
|
+
vi.setSystemTime(10_000);
|
|
24
|
+
expect(time(0)).toBe("10.0s"); // diff = 10_000ms
|
|
25
|
+
|
|
26
|
+
vi.setSystemTime(5_500);
|
|
27
|
+
expect(time(0)).toBe("5.5s"); // diff = 5_500ms
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("formats diff >= 60000ms as minutes", () => {
|
|
31
|
+
vi.setSystemTime(120_000);
|
|
32
|
+
expect(time(0)).toBe("2m"); // diff = 120_000ms
|
|
33
|
+
|
|
34
|
+
vi.setSystemTime(90_000);
|
|
35
|
+
expect(time(0)).toBe("1m"); // diff = 90_000ms (1.5m → floor = 1)
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("appends prefix when provided", () => {
|
|
39
|
+
vi.setSystemTime(500);
|
|
40
|
+
expect(time(0, "ago")).toBe("500ms ago");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("has no prefix when not provided", () => {
|
|
44
|
+
vi.setSystemTime(500);
|
|
45
|
+
expect(time(0)).toBe("500ms");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("diff = 0ms renders as '0ms'", () => {
|
|
49
|
+
vi.setSystemTime(100);
|
|
50
|
+
expect(time(100)).toBe("0ms");
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { formatValue } from "../utils/format-value";
|
|
4
|
+
|
|
5
|
+
describe("formatValue", () => {
|
|
6
|
+
it("formats null", () => {
|
|
7
|
+
expect(formatValue(null)).toBe("null");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("formats undefined", () => {
|
|
11
|
+
expect(formatValue(undefined)).toBe("undefined");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("wraps strings in double quotes", () => {
|
|
15
|
+
expect(formatValue("hello")).toBe('"hello"');
|
|
16
|
+
expect(formatValue("")).toBe('""');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("formats numbers via String()", () => {
|
|
20
|
+
expect(formatValue(42)).toBe("42");
|
|
21
|
+
expect(formatValue(3.14)).toBe("3.14");
|
|
22
|
+
expect(formatValue(0)).toBe("0");
|
|
23
|
+
expect(formatValue(-1)).toBe("-1");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("formats booleans via String()", () => {
|
|
27
|
+
expect(formatValue(true)).toBe("true");
|
|
28
|
+
expect(formatValue(false)).toBe("false");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("JSON-stringifies plain objects", () => {
|
|
32
|
+
expect(formatValue({ a: 1 })).toBe('{"a":1}');
|
|
33
|
+
expect(formatValue({})).toBe("{}");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("JSON-stringifies arrays", () => {
|
|
37
|
+
expect(formatValue([1, 2, 3])).toBe("[1,2,3]");
|
|
38
|
+
expect(formatValue([])).toBe("[]");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("returns '[object]' for circular references", () => {
|
|
42
|
+
const obj: Record<string, unknown> = {};
|
|
43
|
+
obj.self = obj;
|
|
44
|
+
expect(formatValue(obj)).toBe("[object]");
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { beforeEach, describe, it, expect, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { Registry } from "../core/registry";
|
|
4
|
+
|
|
5
|
+
// Reset singleton before each test to keep tests isolated
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
(Registry as unknown as { _instance: Registry | null })._instance = null;
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
describe("Registry singleton", () => {
|
|
11
|
+
it("returns the same instance on subsequent accesses", () => {
|
|
12
|
+
const a = Registry.instance;
|
|
13
|
+
const b = Registry.instance;
|
|
14
|
+
expect(a).toBe(b);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("creates a fresh instance after reset", () => {
|
|
18
|
+
const first = Registry.instance;
|
|
19
|
+
(Registry as unknown as { _instance: Registry | null })._instance = null;
|
|
20
|
+
const second = Registry.instance;
|
|
21
|
+
expect(first).not.toBe(second);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("Registry.registerSignal", () => {
|
|
26
|
+
it("stores the signal and emits 'signal:registered'", () => {
|
|
27
|
+
const registry = Registry.instance;
|
|
28
|
+
const handler = vi.fn();
|
|
29
|
+
registry.bus.on("signal:registered", handler);
|
|
30
|
+
|
|
31
|
+
const instance = {};
|
|
32
|
+
registry.registerSignal(instance, "count", 0, "Counter");
|
|
33
|
+
|
|
34
|
+
const signals = registry.getSignals();
|
|
35
|
+
expect(signals).toHaveLength(1);
|
|
36
|
+
expect(signals[0].label).toBe("count");
|
|
37
|
+
expect(signals[0].value).toBe(0);
|
|
38
|
+
expect(signals[0].componentName).toBe("Counter");
|
|
39
|
+
expect(handler).toHaveBeenCalledOnce();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("builds the signal id as componentId:key", () => {
|
|
43
|
+
const registry = Registry.instance;
|
|
44
|
+
const instance = {};
|
|
45
|
+
registry.registerSignal(instance, "name", "Alice", "MyComp");
|
|
46
|
+
const signal = registry.getSignals()[0];
|
|
47
|
+
expect(signal.id).toMatch(/:.+/);
|
|
48
|
+
expect(signal.id.endsWith(":name")).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("initialises history with the initial value", () => {
|
|
52
|
+
const registry = Registry.instance;
|
|
53
|
+
registry.registerSignal({}, "x", 42, "Comp");
|
|
54
|
+
const signal = registry.getSignals()[0];
|
|
55
|
+
expect(signal.history).toHaveLength(1);
|
|
56
|
+
expect(signal.history[0].value).toBe(42);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("Registry.updateSignal", () => {
|
|
61
|
+
it("updates the value and appends to history", () => {
|
|
62
|
+
const registry = Registry.instance;
|
|
63
|
+
const instance = {};
|
|
64
|
+
registry.registerSignal(instance, "count", 0, "Counter");
|
|
65
|
+
registry.updateSignal(instance, "count", 1, 0);
|
|
66
|
+
|
|
67
|
+
const signal = registry.getSignals()[0];
|
|
68
|
+
expect(signal.value).toBe(1);
|
|
69
|
+
expect(signal.history).toHaveLength(2);
|
|
70
|
+
expect(signal.history[1].value).toBe(1);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("emits 'signal:changed' with old and new value", () => {
|
|
74
|
+
const registry = Registry.instance;
|
|
75
|
+
const handler = vi.fn();
|
|
76
|
+
registry.bus.on("signal:changed", handler);
|
|
77
|
+
|
|
78
|
+
const instance = {};
|
|
79
|
+
registry.registerSignal(instance, "val", "old", "Comp");
|
|
80
|
+
registry.updateSignal(instance, "val", "new", "old");
|
|
81
|
+
|
|
82
|
+
expect(handler).toHaveBeenCalledOnce();
|
|
83
|
+
const payload = handler.mock.calls[0][0] as { entry: { value: unknown }; oldValue: unknown };
|
|
84
|
+
expect(payload.entry.value).toBe("new");
|
|
85
|
+
expect(payload.oldValue).toBe("old");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("caps history at 20 entries", () => {
|
|
89
|
+
const registry = Registry.instance;
|
|
90
|
+
const instance = {};
|
|
91
|
+
registry.registerSignal(instance, "n", 0, "Comp");
|
|
92
|
+
for (let i = 1; i <= 25; i++) {
|
|
93
|
+
registry.updateSignal(instance, "n", i, i - 1);
|
|
94
|
+
}
|
|
95
|
+
const signal = registry.getSignals()[0];
|
|
96
|
+
expect(signal.history.length).toBe(20);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("does nothing when the signal does not exist", () => {
|
|
100
|
+
const registry = Registry.instance;
|
|
101
|
+
const handler = vi.fn();
|
|
102
|
+
registry.bus.on("signal:changed", handler);
|
|
103
|
+
registry.updateSignal({}, "missing", 1, 0);
|
|
104
|
+
expect(handler).not.toHaveBeenCalled();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("pushes a 'signal:change' timeline entry", () => {
|
|
108
|
+
const registry = Registry.instance;
|
|
109
|
+
const instance = {};
|
|
110
|
+
registry.registerSignal(instance, "x", 0, "Comp");
|
|
111
|
+
registry.updateSignal(instance, "x", 5, 0);
|
|
112
|
+
const timeline = registry.getTimeline();
|
|
113
|
+
const entry = timeline.find((t) => t.type === "signal:change");
|
|
114
|
+
expect(entry).toBeDefined();
|
|
115
|
+
expect(entry?.data.new).toBe(5);
|
|
116
|
+
expect(entry?.data.old).toBe(0);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("Registry.registerComponent", () => {
|
|
121
|
+
it("stores the component and emits 'component:registered'", () => {
|
|
122
|
+
const registry = Registry.instance;
|
|
123
|
+
const handler = vi.fn();
|
|
124
|
+
registry.bus.on("component:registered", handler);
|
|
125
|
+
|
|
126
|
+
const instance = {};
|
|
127
|
+
registry.registerComponent(instance, "MyComp");
|
|
128
|
+
|
|
129
|
+
const components = registry.getComponents();
|
|
130
|
+
expect(components).toHaveLength(1);
|
|
131
|
+
expect(components[0].name).toBe("MyComp");
|
|
132
|
+
expect(components[0].renderCount).toBe(0);
|
|
133
|
+
expect(components[0].status).toBe("mounted");
|
|
134
|
+
expect(handler).toHaveBeenCalledOnce();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("Registry.recordRender", () => {
|
|
139
|
+
it("increments renderCount and emits 'component:render'", () => {
|
|
140
|
+
const registry = Registry.instance;
|
|
141
|
+
const handler = vi.fn();
|
|
142
|
+
registry.bus.on("component:render", handler);
|
|
143
|
+
|
|
144
|
+
const instance = {};
|
|
145
|
+
registry.registerComponent(instance, "App");
|
|
146
|
+
registry.recordRender(instance, 5.2);
|
|
147
|
+
|
|
148
|
+
const comp = registry.getComponents()[0];
|
|
149
|
+
expect(comp.renderCount).toBe(1);
|
|
150
|
+
expect(comp.lastRenderDuration).toBe(5.2);
|
|
151
|
+
expect(handler).toHaveBeenCalledOnce();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("accumulates renderCount across multiple renders", () => {
|
|
155
|
+
const registry = Registry.instance;
|
|
156
|
+
const instance = {};
|
|
157
|
+
registry.registerComponent(instance, "App");
|
|
158
|
+
registry.recordRender(instance, 1);
|
|
159
|
+
registry.recordRender(instance, 2);
|
|
160
|
+
registry.recordRender(instance, 3);
|
|
161
|
+
expect(registry.getComponents()[0].renderCount).toBe(3);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("does nothing when the component does not exist", () => {
|
|
165
|
+
const registry = Registry.instance;
|
|
166
|
+
const handler = vi.fn();
|
|
167
|
+
registry.bus.on("component:render", handler);
|
|
168
|
+
registry.recordRender({}, 1);
|
|
169
|
+
expect(handler).not.toHaveBeenCalled();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("pushes a 'component:render' timeline entry", () => {
|
|
173
|
+
const registry = Registry.instance;
|
|
174
|
+
const instance = {};
|
|
175
|
+
registry.registerComponent(instance, "App");
|
|
176
|
+
registry.recordRender(instance, 3.5);
|
|
177
|
+
const entry = registry.getTimeline().find((t) => t.type === "component:render");
|
|
178
|
+
expect(entry).toBeDefined();
|
|
179
|
+
expect(entry?.data.duration).toBe(3.5);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe("Registry.recordLifecycle", () => {
|
|
184
|
+
it("records a lifecycle event and emits 'lifecycle'", () => {
|
|
185
|
+
const registry = Registry.instance;
|
|
186
|
+
const handler = vi.fn();
|
|
187
|
+
registry.bus.on("lifecycle", handler);
|
|
188
|
+
|
|
189
|
+
const instance = {};
|
|
190
|
+
registry.registerComponent(instance, "Comp");
|
|
191
|
+
registry.recordLifecycle(instance, "onMount");
|
|
192
|
+
|
|
193
|
+
expect(handler).toHaveBeenCalledOnce();
|
|
194
|
+
const payload = handler.mock.calls[0][0] as { hook: string; name: string };
|
|
195
|
+
expect(payload.hook).toBe("onMount");
|
|
196
|
+
expect(payload.name).toBe("Comp");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("removes component and its signals on 'onUnmount'", () => {
|
|
200
|
+
const registry = Registry.instance;
|
|
201
|
+
const instance = {};
|
|
202
|
+
registry.registerComponent(instance, "Comp");
|
|
203
|
+
registry.registerSignal(instance, "val", 1, "Comp");
|
|
204
|
+
|
|
205
|
+
expect(registry.getComponents()).toHaveLength(1);
|
|
206
|
+
expect(registry.getSignals()).toHaveLength(1);
|
|
207
|
+
|
|
208
|
+
registry.recordLifecycle(instance, "onUnmount");
|
|
209
|
+
|
|
210
|
+
expect(registry.getComponents()).toHaveLength(0);
|
|
211
|
+
expect(registry.getSignals()).toHaveLength(0);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("emits 'component:unmount' on onUnmount", () => {
|
|
215
|
+
const registry = Registry.instance;
|
|
216
|
+
const handler = vi.fn();
|
|
217
|
+
registry.bus.on("component:unmount", handler);
|
|
218
|
+
|
|
219
|
+
const instance = {};
|
|
220
|
+
registry.registerComponent(instance, "Comp");
|
|
221
|
+
registry.recordLifecycle(instance, "onUnmount");
|
|
222
|
+
|
|
223
|
+
expect(handler).toHaveBeenCalledOnce();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("maps onBeforeMount hook to 'component:mount' timeline type", () => {
|
|
227
|
+
const registry = Registry.instance;
|
|
228
|
+
const instance = {};
|
|
229
|
+
registry.registerComponent(instance, "Comp");
|
|
230
|
+
registry.recordLifecycle(instance, "onBeforeMount");
|
|
231
|
+
const entry = registry.getTimeline().find((t) => t.type === "component:mount");
|
|
232
|
+
expect(entry).toBeDefined();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("maps onUnmount hook to 'component:unmount' timeline type", () => {
|
|
236
|
+
const registry = Registry.instance;
|
|
237
|
+
const instance = {};
|
|
238
|
+
registry.registerComponent(instance, "Comp");
|
|
239
|
+
registry.recordLifecycle(instance, "onUnmount");
|
|
240
|
+
const entry = registry.getTimeline().find((t) => t.type === "component:unmount");
|
|
241
|
+
expect(entry).toBeDefined();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("maps other hooks to 'lifecycle' timeline type", () => {
|
|
245
|
+
const registry = Registry.instance;
|
|
246
|
+
const instance = {};
|
|
247
|
+
registry.registerComponent(instance, "Comp");
|
|
248
|
+
registry.recordLifecycle(instance, "onUpdate");
|
|
249
|
+
const entry = registry.getTimeline().find((t) => t.type === "lifecycle");
|
|
250
|
+
expect(entry).toBeDefined();
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("does nothing when the component does not exist", () => {
|
|
254
|
+
const registry = Registry.instance;
|
|
255
|
+
const handler = vi.fn();
|
|
256
|
+
registry.bus.on("lifecycle", handler);
|
|
257
|
+
registry.recordLifecycle({}, "onMount");
|
|
258
|
+
expect(handler).not.toHaveBeenCalled();
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe("Registry.recordMethodCall", () => {
|
|
263
|
+
it("pushes a 'method:call' timeline entry", () => {
|
|
264
|
+
const registry = Registry.instance;
|
|
265
|
+
const timelineHandler = vi.fn();
|
|
266
|
+
registry.bus.on("timeline:push", timelineHandler);
|
|
267
|
+
|
|
268
|
+
const instance = {};
|
|
269
|
+
registry.recordMethodCall(instance, "doSomething", [1, 2], "result", 3.14, "MyComp");
|
|
270
|
+
|
|
271
|
+
const entry = registry.getTimeline().find((t) => t.type === "method:call");
|
|
272
|
+
expect(entry).toBeDefined();
|
|
273
|
+
expect(entry?.label).toBe("MyComp.doSomething()");
|
|
274
|
+
expect(entry?.data.args).toEqual([1, 2]);
|
|
275
|
+
expect(entry?.data.result).toBe("result");
|
|
276
|
+
expect(entry?.data.duration).toBe(3.14);
|
|
277
|
+
expect(timelineHandler).toHaveBeenCalledOnce();
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
describe("Registry.getSignalsByComponent", () => {
|
|
282
|
+
it("returns only signals belonging to the given component", () => {
|
|
283
|
+
const registry = Registry.instance;
|
|
284
|
+
const inst1 = {};
|
|
285
|
+
const inst2 = {};
|
|
286
|
+
registry.registerComponent(inst1, "Comp1");
|
|
287
|
+
registry.registerComponent(inst2, "Comp2");
|
|
288
|
+
registry.registerSignal(inst1, "a", 1, "Comp1");
|
|
289
|
+
registry.registerSignal(inst2, "b", 2, "Comp2");
|
|
290
|
+
|
|
291
|
+
const comp1Id = registry.getComponents().find((c) => c.name === "Comp1")!.id;
|
|
292
|
+
const signals = registry.getSignalsByComponent(comp1Id);
|
|
293
|
+
expect(signals).toHaveLength(1);
|
|
294
|
+
expect(signals[0].label).toBe("a");
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
describe("Registry.recordLifecycle — onUnmount cross-component isolation", () => {
|
|
299
|
+
it("only removes signals belonging to the unmounted component", () => {
|
|
300
|
+
const registry = Registry.instance;
|
|
301
|
+
const inst1 = {};
|
|
302
|
+
const inst2 = {};
|
|
303
|
+
registry.registerComponent(inst1, "Comp1");
|
|
304
|
+
registry.registerComponent(inst2, "Comp2");
|
|
305
|
+
registry.registerSignal(inst1, "a", 1, "Comp1");
|
|
306
|
+
registry.registerSignal(inst2, "b", 2, "Comp2");
|
|
307
|
+
|
|
308
|
+
registry.recordLifecycle(inst1, "onUnmount");
|
|
309
|
+
|
|
310
|
+
// inst1's signal is gone, inst2's signal remains
|
|
311
|
+
const remaining = registry.getSignals();
|
|
312
|
+
expect(remaining).toHaveLength(1);
|
|
313
|
+
expect(remaining[0].label).toBe("b");
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
describe("Registry timeline cap", () => {
|
|
318
|
+
it("caps timeline at 200 entries", () => {
|
|
319
|
+
const registry = Registry.instance;
|
|
320
|
+
const instance = {};
|
|
321
|
+
registry.registerComponent(instance, "Comp");
|
|
322
|
+
|
|
323
|
+
// Each updateSignal pushes one timeline entry (plus the registerSignal = 0 entries)
|
|
324
|
+
registry.registerSignal(instance, "val", 0, "Comp");
|
|
325
|
+
for (let i = 1; i <= 210; i++) {
|
|
326
|
+
registry.updateSignal(instance, "val", i, i - 1);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
expect(registry.getTimeline().length).toBe(200);
|
|
330
|
+
});
|
|
331
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { beforeEach, describe, it, expect, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { Registry } from "../core/registry";
|
|
4
|
+
import { Trace } from "../decorators/trace";
|
|
5
|
+
|
|
6
|
+
// Reset the Registry singleton before each test
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
(Registry as unknown as { _instance: Registry | null })._instance = null;
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
14
|
+
type AnyConstructor = new (...args: any[]) => any;
|
|
15
|
+
|
|
16
|
+
function applyTrace<T extends AnyConstructor>(cls: T): T {
|
|
17
|
+
return Trace()(cls, {} as ClassDecoratorContext);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ── Trace decorator ───────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
describe("Trace — render()", () => {
|
|
23
|
+
it("records a render event after calling render()", () => {
|
|
24
|
+
class MyComp {
|
|
25
|
+
render() { return "html"; }
|
|
26
|
+
}
|
|
27
|
+
const Traced = applyTrace(MyComp);
|
|
28
|
+
const instance = new Traced() as { render: () => string };
|
|
29
|
+
|
|
30
|
+
const registry = Registry.instance;
|
|
31
|
+
registry.registerComponent(instance, "MyComp");
|
|
32
|
+
|
|
33
|
+
instance.render();
|
|
34
|
+
|
|
35
|
+
const renders = registry.getTimeline().filter((e) => e.type === "component:render");
|
|
36
|
+
expect(renders).toHaveLength(1);
|
|
37
|
+
expect(renders[0].label).toBe("<MyComp>");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("returns the original render result", () => {
|
|
41
|
+
class Box {
|
|
42
|
+
render() { return "<div>box</div>"; }
|
|
43
|
+
}
|
|
44
|
+
const Traced = applyTrace(Box);
|
|
45
|
+
const instance = new Traced() as { render: () => string };
|
|
46
|
+
|
|
47
|
+
Registry.instance.registerComponent(instance, "Box");
|
|
48
|
+
expect(instance.render()).toBe("<div>box</div>");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("wraps render even when it takes arguments", () => {
|
|
52
|
+
class Comp {
|
|
53
|
+
render(ctx: string) { return ctx; }
|
|
54
|
+
}
|
|
55
|
+
const Traced = applyTrace(Comp);
|
|
56
|
+
const instance = new Traced() as { render: (ctx: string) => string };
|
|
57
|
+
|
|
58
|
+
Registry.instance.registerComponent(instance, "Comp");
|
|
59
|
+
const result = instance.render("ctx-arg");
|
|
60
|
+
expect(result).toBe("ctx-arg");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("does not wrap render when the class has no render method", () => {
|
|
64
|
+
class NoRender {}
|
|
65
|
+
const Traced = applyTrace(NoRender);
|
|
66
|
+
const instance = new Traced() as Record<string, unknown>;
|
|
67
|
+
expect(instance.render).toBeUndefined();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("Trace — onBeforeMount()", () => {
|
|
72
|
+
it("registers the component in the registry on onBeforeMount", () => {
|
|
73
|
+
class Widget {
|
|
74
|
+
onBeforeMount() {}
|
|
75
|
+
}
|
|
76
|
+
const Traced = applyTrace(Widget);
|
|
77
|
+
const instance = new Traced() as { onBeforeMount: () => void };
|
|
78
|
+
|
|
79
|
+
const registry = Registry.instance;
|
|
80
|
+
instance.onBeforeMount();
|
|
81
|
+
|
|
82
|
+
const components = registry.getComponents();
|
|
83
|
+
expect(components).toHaveLength(1);
|
|
84
|
+
expect(components[0].name).toBe("Widget");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("calls the original onBeforeMount when it exists", () => {
|
|
88
|
+
const originalSpy = vi.fn();
|
|
89
|
+
class Panel {
|
|
90
|
+
onBeforeMount() { originalSpy(); }
|
|
91
|
+
}
|
|
92
|
+
const Traced = applyTrace(Panel);
|
|
93
|
+
const instance = new Traced() as { onBeforeMount: () => void };
|
|
94
|
+
|
|
95
|
+
instance.onBeforeMount();
|
|
96
|
+
expect(originalSpy).toHaveBeenCalled();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("records a component:mount timeline event on onBeforeMount", () => {
|
|
100
|
+
class App {
|
|
101
|
+
onBeforeMount() {}
|
|
102
|
+
}
|
|
103
|
+
const Traced = applyTrace(App);
|
|
104
|
+
const instance = new Traced() as { onBeforeMount: () => void };
|
|
105
|
+
|
|
106
|
+
instance.onBeforeMount();
|
|
107
|
+
|
|
108
|
+
const mounts = Registry.instance.getTimeline().filter((e) => e.type === "component:mount");
|
|
109
|
+
expect(mounts).toHaveLength(1);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("Trace — lifecycle hooks", () => {
|
|
114
|
+
const hooks = ["onMount", "onUnmount", "onBeforeUpdate", "onUpdate", "onAfterUpdate"] as const;
|
|
115
|
+
|
|
116
|
+
for (const hook of hooks) {
|
|
117
|
+
it(`wraps ${hook} and records the lifecycle event`, () => {
|
|
118
|
+
class Comp {
|
|
119
|
+
onBeforeMount() {}
|
|
120
|
+
}
|
|
121
|
+
const Traced = applyTrace(Comp);
|
|
122
|
+
const instance = new Traced() as unknown as Record<string, () => void>;
|
|
123
|
+
|
|
124
|
+
// Register the component first via onBeforeMount
|
|
125
|
+
instance.onBeforeMount();
|
|
126
|
+
|
|
127
|
+
instance[hook]?.();
|
|
128
|
+
|
|
129
|
+
const events = Registry.instance.getTimeline().filter(
|
|
130
|
+
(e) => e.data.hook === hook,
|
|
131
|
+
);
|
|
132
|
+
expect(events.length).toBeGreaterThanOrEqual(1);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it(`${hook}: calls original implementation if it exists`, () => {
|
|
136
|
+
const spy = vi.fn();
|
|
137
|
+
class Comp {
|
|
138
|
+
onBeforeMount() {}
|
|
139
|
+
}
|
|
140
|
+
(Comp.prototype as unknown as Record<string, unknown>)[hook] = spy;
|
|
141
|
+
const Traced = applyTrace(Comp);
|
|
142
|
+
const instance = new Traced() as unknown as Record<string, () => void>;
|
|
143
|
+
|
|
144
|
+
instance.onBeforeMount();
|
|
145
|
+
instance[hook]?.();
|
|
146
|
+
expect(spy).toHaveBeenCalled();
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
});
|