slicejs-web-framework 4.0.2 → 4.1.1

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 (73) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +97 -97
  3. package/Slice/Components/Structural/ContextManager/ContextManager.js +422 -422
  4. package/Slice/Components/Structural/ContextManager/ContextManagerDebugger.js +470 -470
  5. package/Slice/Components/Structural/Controller/Controller.js +1406 -1406
  6. package/Slice/Components/Structural/Controller/allowedValuesValidation.js +52 -52
  7. package/Slice/Components/Structural/Debugger/Debugger.html +72 -72
  8. package/Slice/Components/Structural/Debugger/Debugger.js +1505 -1505
  9. package/Slice/Components/Structural/EventManager/EventManager.js +651 -651
  10. package/Slice/Components/Structural/EventManager/EventManagerDebugger.js +985 -985
  11. package/Slice/Components/Structural/LeakInspector/LeakInspector.js +296 -296
  12. package/Slice/Components/Structural/Logger/Log.js +10 -10
  13. package/Slice/Components/Structural/Logger/LogViewer/LogViewer.js +662 -662
  14. package/Slice/Components/Structural/Logger/Logger.js +225 -225
  15. package/Slice/Components/Structural/Router/Router.js +820 -810
  16. package/Slice/Components/Structural/Router/applyRouteMetadata.js +39 -0
  17. package/Slice/Components/Structural/StylesManager/StylesManager.js +82 -82
  18. package/Slice/Components/Structural/StylesManager/ThemeManager/ThemeManager.js +175 -175
  19. package/Slice/Slice.js +711 -710
  20. package/api/framework/devDepsOptimizer.js +335 -335
  21. package/api/framework/express.js +3 -3
  22. package/api/framework/precompressedStatic.js +102 -93
  23. package/api/framework/publicEnvResolver.js +123 -123
  24. package/api/framework/securityMiddleware.js +197 -197
  25. package/api/framework/seo.js +387 -0
  26. package/api/framework/server.js +777 -410
  27. package/api/index.js +14 -4
  28. package/package.json +6 -2
  29. package/sliceConfig.schema.json +122 -122
  30. package/src/App/index.html +69 -69
  31. package/src/App/index.js +20 -20
  32. package/src/App/style.css +11 -11
  33. package/src/Components/AppComponents/AboutSection/AboutSection.css +9 -9
  34. package/src/Components/AppComponents/AboutSection/AboutSection.html +8 -8
  35. package/src/Components/AppComponents/AboutSection/AboutSection.js +12 -12
  36. package/src/Components/AppComponents/AppShell/AppShell.css +10 -10
  37. package/src/Components/AppComponents/AppShell/AppShell.html +4 -4
  38. package/src/Components/AppComponents/AppShell/AppShell.js +36 -36
  39. package/src/Components/AppComponents/HomeSection/HomeSection.css +20 -20
  40. package/src/Components/AppComponents/HomeSection/HomeSection.html +10 -10
  41. package/src/Components/AppComponents/HomeSection/HomeSection.js +19 -19
  42. package/src/Components/components.js +8 -8
  43. package/src/public/Styles/sliceStyles.css +34 -34
  44. package/src/public/Themes/Dark.css +42 -42
  45. package/src/public/Themes/Light.css +31 -31
  46. package/src/public/Themes/Slice.css +47 -47
  47. package/src/public/manifest.json +15 -15
  48. package/src/public/robots.txt +4 -4
  49. package/src/public/sitemap.xml +8 -8
  50. package/src/routes.js +9 -9
  51. package/src/sliceConfig.json +72 -72
  52. package/types/index.d.ts +219 -219
  53. package/.opencode/opencode.json +0 -15
  54. package/Slice/tests/build-bundled-component-without-category.test.js +0 -103
  55. package/Slice/tests/build-js-only-visual-components.test.js +0 -144
  56. package/Slice/tests/build-singleton.test.js +0 -244
  57. package/Slice/tests/bundle-v2-runtime-contract.test.js +0 -747
  58. package/Slice/tests/context-patch-use.test.js +0 -95
  59. package/Slice/tests/destroy-cascade.test.js +0 -215
  60. package/Slice/tests/event-emitters.test.js +0 -79
  61. package/Slice/tests/event-registry.test.js +0 -141
  62. package/Slice/tests/fixtures/real-runtime-loader.mjs +0 -21
  63. package/Slice/tests/leak-orphans.test.js +0 -131
  64. package/Slice/tests/props-allowed-values-validation.test.js +0 -119
  65. package/Slice/tests/public-env-runtime-accessors.test.js +0 -44
  66. package/Slice/tests/public-env-typed-accessors.test.js +0 -66
  67. package/Slice/tests/router-loading-finally.test.js +0 -68
  68. package/api/tests/dev-deps-optimizer.test.js +0 -137
  69. package/api/tests/dev-server-external.test.js +0 -56
  70. package/api/tests/precompressed-static.test.js +0 -119
  71. package/api/tests/public-env-resolver.test.js +0 -193
  72. package/tests/router-build-failure.test.js +0 -125
  73. package/tests/router-emit-ordering.test.js +0 -63
@@ -1,95 +0,0 @@
1
- import test from 'node:test';
2
- import assert from 'node:assert/strict';
3
-
4
- globalThis.alert = () => {};
5
-
6
- // Minimal global `slice` so ContextManager's slice.logger / slice.events work in
7
- // isolation (a tiny in-memory event bus drives the watch notifications).
8
- const subscribers = new Map(); // eventName -> Set<fn>
9
- let subId = 0;
10
- globalThis.slice = {
11
- logger: { logError() {}, logInfo() {}, logWarning() {} },
12
- events: {
13
- subscribe(name, cb) {
14
- if (!subscribers.has(name)) subscribers.set(name, new Set());
15
- subscribers.get(name).add(cb);
16
- return `sub-${++subId}`;
17
- },
18
- emit(name, ...args) {
19
- for (const cb of subscribers.get(name) ?? []) cb(...args);
20
- },
21
- },
22
- };
23
-
24
- const { default: ContextManager } = await import(
25
- '../Components/Structural/ContextManager/ContextManager.js'
26
- );
27
-
28
- function freshContext(name, initial) {
29
- const ctx = new ContextManager();
30
- ctx.create(name, initial);
31
- return ctx;
32
- }
33
-
34
- test('patch merges into existing state (does not replace)', () => {
35
- const ctx = freshContext('cart', { items: 2, total: 47, discount: 0 });
36
- ctx.patch('cart', { discount: 0.1 });
37
- assert.deepEqual(ctx.getState('cart'), { items: 2, total: 47, discount: 0.1 });
38
- });
39
-
40
- test('setState replaces, patch keeps the rest', () => {
41
- const ctx = freshContext('s', { a: 1, b: 2 });
42
- ctx.setState('s', { a: 9 }); // replace → drops b
43
- assert.deepEqual(ctx.getState('s'), { a: 9 });
44
- ctx.patch('s', { b: 5 }); // merge onto { a: 9 }
45
- assert.deepEqual(ctx.getState('s'), { a: 9, b: 5 });
46
- });
47
-
48
- test('patch rejects non-object partials and missing contexts (no throw)', () => {
49
- const ctx = freshContext('s', { a: 1 });
50
- ctx.patch('s', null);
51
- ctx.patch('s', [1, 2]);
52
- ctx.patch('nope', { x: 1 });
53
- assert.deepEqual(ctx.getState('s'), { a: 1 });
54
- assert.equal(ctx.has('nope'), false);
55
- });
56
-
57
- test('use(name) returns a handle bound to that context', () => {
58
- const ctx = freshContext('settings', { model: 'a' });
59
- const settings = ctx.use('settings');
60
-
61
- assert.equal(settings.get().model, 'a');
62
- settings.set({ model: 'b' });
63
- assert.equal(ctx.getState('settings').model, 'b');
64
- settings.patch({ theme: 'dark' });
65
- assert.deepEqual(ctx.getState('settings'), { model: 'b', theme: 'dark' });
66
- assert.equal(settings.has(), true);
67
- settings.destroy();
68
- assert.equal(ctx.has('settings'), false);
69
- });
70
-
71
- test('use(name).bind calls back immediately and on every change', () => {
72
- const ctx = freshContext('cart', { items: [], n: 0 });
73
- const calls = [];
74
- ctx.use('cart').bind({ sliceId: 'c1' }, (state) => calls.push(state.n));
75
-
76
- assert.deepEqual(calls, [0]); // immediate initial call
77
- ctx.patch('cart', { n: 1 });
78
- assert.deepEqual(calls, [0, 1]); // fired on change
79
- });
80
-
81
- test('use(name).bind with a selector only fires when the selected value changes', () => {
82
- const ctx = freshContext('cart', { items: ['x'], note: 'a' });
83
- const counts = [];
84
- ctx.use('cart').bind(
85
- { sliceId: 'c2' },
86
- (count) => counts.push(count),
87
- (s) => s.items.length
88
- );
89
-
90
- assert.deepEqual(counts, [1]); // immediate, selected value = length
91
- ctx.patch('cart', { note: 'b' }); // length unchanged → no fire
92
- assert.deepEqual(counts, [1]);
93
- ctx.patch('cart', { items: ['x', 'y'] }); // length changed → fire
94
- assert.deepEqual(counts, [1, 2]);
95
- });
@@ -1,215 +0,0 @@
1
- // Real-Controller tests for destroyComponent cascading to children.
2
- //
3
- // Loads the REAL Controller (via the resolve hook that stubs its
4
- // '/Components/components.js' import) and drives it with a minimal fake DOM, so
5
- // the actual registerComponentsRecursively + destroyComponent code runs. This
6
- // reproduces the childrenIndex bug and verifies the fix: destroyComponent(parent)
7
- // must cascade to nested Visual children, running their beforeDestroy.
8
- import { register } from 'node:module';
9
- import test from 'node:test';
10
- import assert from 'node:assert/strict';
11
-
12
- globalThis.alert = () => {};
13
- register(new URL('./fixtures/real-runtime-loader.mjs', import.meta.url));
14
-
15
- // The controller references the `slice` global for logging / events.
16
- globalThis.slice = {
17
- logger: { logError() {}, logWarning() {}, logInfo() {} },
18
- events: null
19
- };
20
-
21
- const { default: Controller } = await import('../Components/Structural/Controller/Controller.js');
22
-
23
- // --- minimal fake DOM ----------------------------------------------------
24
- // Each node is a SLICE-* element with querySelectorAll('*') returning all of
25
- // its descendants in document (pre) order — enough for registerComponentsRecursively.
26
- let destroyLog = [];
27
-
28
- function mkEl(sliceId, children = []) {
29
- const node = {
30
- sliceId,
31
- tagName: 'SLICE-X',
32
- isConnected: true,
33
- parentComponent: null,
34
- _children: children,
35
- querySelectorAll() {
36
- const out = [];
37
- const walk = (n) => {
38
- for (const c of n._children) {
39
- out.push(c);
40
- walk(c);
41
- }
42
- };
43
- walk(node);
44
- return out;
45
- },
46
- remove() {
47
- this.isConnected = false;
48
- },
49
- beforeDestroy() {
50
- destroyLog.push(this.sliceId);
51
- }
52
- };
53
- return node;
54
- }
55
-
56
- // Collect every node in a tree (pre-order).
57
- function flatten(root) {
58
- const out = [root];
59
- for (const c of root._children) out.push(...flatten(c));
60
- return out;
61
- }
62
-
63
- // Simulate slice.build's registration: registerComponent WITHOUT a parent for
64
- // every node, then registerComponentsRecursively on the root (what _build does
65
- // for a Visual once its subtree is in the DOM).
66
- function buildTree(controller, root) {
67
- for (const node of flatten(root)) controller.registerComponent(node);
68
- controller.registerComponentsRecursively(root);
69
- }
70
-
71
- function setup() {
72
- destroyLog = [];
73
- return new Controller();
74
- }
75
-
76
- test('destroyComponent(parent) cascades to a nested child', () => {
77
- const c = setup();
78
- const child = mkEl('child');
79
- const parent = mkEl('parent', [child]);
80
- buildTree(c, parent);
81
-
82
- assert.equal(c.activeComponents.has('child'), true, 'child registered');
83
-
84
- const count = c.destroyComponent(parent);
85
-
86
- assert.equal(c.activeComponents.has('parent'), false, 'parent removed');
87
- assert.equal(c.activeComponents.has('child'), false, 'child removed (cascaded)');
88
- assert.ok(destroyLog.includes('child'), 'child beforeDestroy ran');
89
- assert.equal(count, 2, 'reported 2 destroyed');
90
- });
91
-
92
- test('cascade reaches deep descendants, deepest beforeDestroy first', () => {
93
- const c = setup();
94
- const grandchild = mkEl('grandchild');
95
- const child = mkEl('child', [grandchild]);
96
- const parent = mkEl('parent', [child]);
97
- buildTree(c, parent);
98
-
99
- c.destroyComponent(parent);
100
-
101
- assert.equal(c.activeComponents.size, 0, 'all three destroyed');
102
- // deepest-first ordering (children before their parents)
103
- assert.ok(
104
- destroyLog.indexOf('grandchild') < destroyLog.indexOf('child'),
105
- 'grandchild before child'
106
- );
107
- assert.ok(destroyLog.indexOf('child') < destroyLog.indexOf('parent'), 'child before parent');
108
- });
109
-
110
- test('childrenIndex records DIRECT parents, not ancestors', () => {
111
- const c = setup();
112
- const grandchild = mkEl('grandchild');
113
- const child = mkEl('child', [grandchild]);
114
- const parent = mkEl('parent', [child]);
115
- buildTree(c, parent);
116
-
117
- assert.deepEqual([...(c.childrenIndex.get('parent') || [])], ['child'], 'parent -> child');
118
- assert.deepEqual([...(c.childrenIndex.get('child') || [])], ['grandchild'], 'child -> grandchild');
119
- assert.equal(child.parentComponent.sliceId, 'parent');
120
- assert.equal(grandchild.parentComponent.sliceId, 'child', 'direct parent, not the root');
121
- });
122
-
123
- test('destroying a subtree leaves no stale childrenIndex entries', () => {
124
- const c = setup();
125
- const child = mkEl('child');
126
- const parent = mkEl('parent', [child]);
127
- buildTree(c, parent);
128
-
129
- c.destroyComponent(parent);
130
-
131
- assert.equal(c.childrenIndex.has('parent'), false, 'parent index entry cleared');
132
- assert.equal(c.childrenIndex.has('child'), false, 'child index entry cleared');
133
- });
134
-
135
- test('a Service (no DOM element) is NOT cascaded — still needs explicit destroy', () => {
136
- const c = setup();
137
- const child = mkEl('child');
138
- const parent = mkEl('parent', [child]);
139
- buildTree(c, parent);
140
- // A Service built by the parent: registered, but not part of any DOM subtree.
141
- const service = { sliceId: 'svc', isConnected: false, beforeDestroy() { destroyLog.push('svc'); } };
142
- c.registerComponent(service);
143
-
144
- c.destroyComponent(parent);
145
-
146
- assert.equal(c.activeComponents.has('svc'), true, 'service survives parent destroy');
147
- assert.equal(destroyLog.includes('svc'), false, 'service beforeDestroy did NOT run');
148
- });
149
-
150
- test('all siblings under one parent cascade', () => {
151
- const c = setup();
152
- const a = mkEl('a');
153
- const b = mkEl('b');
154
- const parent = mkEl('parent', [a, b]);
155
- buildTree(c, parent);
156
-
157
- assert.deepEqual([...c.childrenIndex.get('parent')].sort(), ['a', 'b']);
158
-
159
- c.destroyComponent(parent);
160
- assert.equal(c.activeComponents.size, 0, 'parent and both siblings destroyed');
161
- });
162
-
163
- test('destroying a mid-level child destroys only its subtree', () => {
164
- const c = setup();
165
- const grandchild = mkEl('grandchild');
166
- const child = mkEl('child', [grandchild]);
167
- const parent = mkEl('parent', [child]);
168
- buildTree(c, parent);
169
-
170
- const count = c.destroyComponent(child);
171
-
172
- assert.equal(count, 2, 'child + grandchild');
173
- assert.equal(c.activeComponents.has('parent'), true, 'parent survives');
174
- assert.equal(c.activeComponents.has('child'), false);
175
- assert.equal(c.activeComponents.has('grandchild'), false);
176
- assert.equal(
177
- c.childrenIndex.has('parent') && c.childrenIndex.get('parent').has('child'),
178
- false,
179
- "parent's index no longer points to the destroyed child"
180
- );
181
- });
182
-
183
- test('re-running registerComponentsRecursively is idempotent', () => {
184
- const c = setup();
185
- const child = mkEl('child');
186
- const parent = mkEl('parent', [child]);
187
- buildTree(c, parent);
188
- c.registerComponentsRecursively(parent); // walk again
189
-
190
- assert.deepEqual([...c.childrenIndex.get('parent')], ['child'], 'no duplicate edges');
191
-
192
- c.destroyComponent(parent);
193
- assert.equal(c.activeComponents.size, 0, 'still cascades cleanly');
194
- });
195
-
196
- test('destroyByContainer also collects the whole DOM subtree', () => {
197
- const c = setup();
198
- const grandchild = mkEl('grandchild');
199
- const child = mkEl('child', [grandchild]);
200
- const parent = mkEl('parent', [child]);
201
- buildTree(c, parent);
202
-
203
- // destroyByContainer queries [slice-id] on the container; emulate that query.
204
- const container = {
205
- querySelectorAll() {
206
- return flatten(parent).map((n) => ({
207
- getAttribute: () => n.sliceId,
208
- sliceId: n.sliceId
209
- }));
210
- }
211
- };
212
-
213
- const count = c.destroyByContainer(container);
214
- assert.equal(count, 3, 'all three destroyed via DOM discovery');
215
- });
@@ -1,79 +0,0 @@
1
- // Tests for emitter tracing: who emits each event (bind attribution + dev
2
- // stack-trace fallback), recorded only while a debugger is "open" (recording).
3
- import test from 'node:test';
4
- import assert from 'node:assert/strict';
5
-
6
- globalThis.slice = {
7
- isProduction: () => false,
8
- controller: { activeComponents: new Map() },
9
- logger: { logError() {}, logInfo() {}, logWarning() {}, info() {}, error() {} },
10
- };
11
-
12
- const { default: EventManager } = await import('../Components/Structural/EventManager/EventManager.js');
13
-
14
- test('bound emit is attributed to its component', () => {
15
- const em = new EventManager();
16
- em.startRecording();
17
- const comp = { sliceId: 'card-1', constructor: { name: 'Card' } };
18
- em.bind(comp).emit('user:login', { id: 1 });
19
-
20
- const bucket = em.emitters.get('user:login');
21
- assert.ok(bucket, 'event has emitters bucket');
22
- const entry = bucket.get('card-1');
23
- assert.equal(entry.name, 'Card');
24
- assert.equal(entry.sliceId, 'card-1');
25
- assert.equal(entry.count, 1);
26
-
27
- const hist = em.emitHistory.find((e) => e.eventName === 'user:login');
28
- assert.equal(hist.emitter.sliceId, 'card-1', 'history entry carries the emitter');
29
- });
30
-
31
- test('global emit falls back to a dev stack-trace source', () => {
32
- const em = new EventManager();
33
- em.startRecording();
34
- em.emit('router:change');
35
-
36
- const hist = em.emitHistory.find((e) => e.eventName === 'router:change');
37
- assert.ok(hist.emitter, 'emitter captured for global emit');
38
- assert.ok(hist.emitter.source && hist.emitter.source.includes('.js'), 'source is file:line');
39
- assert.ok(!hist.emitter.source.includes('EventManager.js'), 'attributes the caller, not the manager');
40
- });
41
-
42
- test('emitter counts accumulate per distinct source', () => {
43
- const em = new EventManager();
44
- em.startRecording();
45
- em.bind({ sliceId: 'a-1', constructor: { name: 'A' } }).emit('x:y');
46
- em.bind({ sliceId: 'a-1', constructor: { name: 'A' } }).emit('x:y');
47
- em.bind({ sliceId: 'b-2', constructor: { name: 'B' } }).emit('x:y');
48
-
49
- const bucket = em.emitters.get('x:y');
50
- assert.equal(bucket.get('a-1').count, 2);
51
- assert.equal(bucket.get('b-2').count, 1);
52
- });
53
-
54
- test('no recording (debugger closed) => no emitter tracking, zero overhead', () => {
55
- const em = new EventManager();
56
- em.emit('a:b');
57
- em.bind({ sliceId: 'z-1', constructor: { name: 'Z' } }).emit('a:b');
58
- assert.equal(em.emitters.size, 0);
59
- assert.equal(em.emitHistory.length, 0);
60
- });
61
-
62
- test('production: no stack-trace source captured', () => {
63
- globalThis.slice.isProduction = () => true;
64
- const em = new EventManager();
65
- em.startRecording();
66
- em.emit('p:q');
67
- const hist = em.emitHistory.find((e) => e.eventName === 'p:q');
68
- assert.equal(hist.emitter, null, 'no source attribution in production');
69
- globalThis.slice.isProduction = () => false;
70
- });
71
-
72
- test('startRecording resets the emitters map', () => {
73
- const em = new EventManager();
74
- em.startRecording();
75
- em.bind({ sliceId: 'a-1', constructor: { name: 'A' } }).emit('x:y');
76
- assert.equal(em.emitters.size, 1);
77
- em.startRecording();
78
- assert.equal(em.emitters.size, 0, 'fresh session clears prior emitters');
79
- });
@@ -1,141 +0,0 @@
1
- // Unit tests for the EventManager typed-event registry (register/validate/warn).
2
- // EventManager only depends on the `slice` global for logging + isProduction +
3
- // controller.activeComponents, so we stub a minimal one and drive the real class.
4
- import test from 'node:test';
5
- import assert from 'node:assert/strict';
6
-
7
- const warnings = [];
8
- globalThis.slice = {
9
- isProduction: () => false,
10
- controller: { activeComponents: new Map() },
11
- logger: {
12
- logError() {},
13
- logInfo() {},
14
- logWarning(_c, message) { warnings.push(message); },
15
- info() {}, error() {}, warn() {},
16
- },
17
- };
18
-
19
- const { default: EventManager } = await import('../Components/Structural/EventManager/EventManager.js');
20
-
21
- const undeclaredWarns = (name) => warnings.filter((w) => w.includes('Undeclared') && (!name || w.includes(name)));
22
-
23
- test('loose mode: with no registry, nothing warns', () => {
24
- warnings.length = 0;
25
- const em = new EventManager();
26
- em.emit('anything:happens');
27
- em.subscribe('whatever:event', () => {});
28
- assert.equal(undeclaredWarns().length, 0);
29
- });
30
-
31
- test('declared event emits and subscribes without warning', () => {
32
- warnings.length = 0;
33
- const em = new EventManager();
34
- em.register({ 'user:login': { payload: { id: 'number' } } });
35
- em.subscribe('user:login', () => {});
36
- em.emit('user:login', { id: 1 });
37
- assert.equal(undeclaredWarns().length, 0);
38
- });
39
-
40
- test('undeclared event warns exactly once but still delivers', () => {
41
- warnings.length = 0;
42
- const em = new EventManager();
43
- em.register({ 'user:login': { payload: null } });
44
-
45
- let calls = 0;
46
- em.subscribe('ghost:event', () => { calls += 1; }); // subscribe (undeclared) -> warns
47
- em.emit('ghost:event'); // emit (undeclared, already warned)
48
- em.emit('ghost:event');
49
-
50
- assert.equal(undeclaredWarns('ghost:event').length, 1, 'warned exactly once');
51
- assert.equal(calls, 2, 'delivery still happened both emits');
52
- });
53
-
54
- test('framework events (router:*/context:*) never warn', () => {
55
- warnings.length = 0;
56
- const em = new EventManager();
57
- em.register({ 'user:login': { payload: null } });
58
- em.emit('router:change');
59
- em.emit('context:user', {});
60
- em.subscribe('context:cart', () => {});
61
- assert.equal(undeclaredWarns().length, 0);
62
- });
63
-
64
- test('production: undeclared events do not warn', () => {
65
- warnings.length = 0;
66
- globalThis.slice.isProduction = () => true;
67
- const em = new EventManager();
68
- em.register({ 'a:b': { payload: null } });
69
- em.emit('zzz:undeclared');
70
- assert.equal(undeclaredWarns().length, 0);
71
- globalThis.slice.isProduction = () => false;
72
- });
73
-
74
- test('register() merges catalogs and is chainable', () => {
75
- const em = new EventManager();
76
- const ret = em.register({ 'a:1': { payload: null } }).register({ 'b:2': { payload: { x: 'string' } } });
77
- assert.equal(ret, em, 'returns the manager for chaining');
78
- assert.ok(em.registry.has('a:1') && em.registry.has('b:2'), 'both catalogs merged');
79
- assert.equal(em.isDeclared('a:1'), true);
80
- assert.equal(em.isDeclared('nope:nope'), false);
81
- });
82
-
83
- test('register(namespace, catalog) prefixes keys', () => {
84
- const em = new EventManager();
85
- em.register('user', { login: { payload: { id: 'number' } }, logout: { payload: null } });
86
- assert.ok(em.registry.has('user:login'), 'user:login declared');
87
- assert.ok(em.registry.has('user:logout'), 'user:logout declared');
88
- assert.equal(em.namespaceOf('user:login'), 'user');
89
- assert.equal(em.namespaceOf('ready'), null);
90
- });
91
-
92
- test('event without namespace warns once in dev', () => {
93
- warnings.length = 0;
94
- const em = new EventManager();
95
- em.register({ 'ready': { payload: null } }); // declared but no namespace -> warns
96
- em.emit('ready');
97
- em.emit('ready');
98
- const nsWarns = warnings.filter((w) => w.includes('no namespace'));
99
- assert.equal(nsWarns.length, 1, 'namespace warning fired exactly once');
100
- });
101
-
102
- test('no-namespace warning is silent in loose mode (no registry)', () => {
103
- warnings.length = 0;
104
- const em = new EventManager();
105
- em.emit('ready'); // nobody registered -> loose mode, no warnings at all
106
- assert.equal(warnings.length, 0);
107
- });
108
-
109
- test('loadGraph stores the static graph and exposes emitters/listeners', () => {
110
- const em = new EventManager();
111
- assert.deepEqual(em.staticEmittersOf('user:login'), [], 'empty before loadGraph');
112
- em.loadGraph({
113
- events: {
114
- 'user:login': {
115
- payload: { id: 'number' },
116
- emitters: [{ file: 'src/Login.js', line: 12, component: 'Login' }],
117
- listeners: [{ file: 'src/Header.js', line: 5, component: 'Header' }],
118
- },
119
- },
120
- dynamic: { emitters: [], listeners: [] },
121
- });
122
- assert.equal(em.staticEmittersOf('user:login')[0].component, 'Login');
123
- assert.equal(em.staticListenersOf('user:login')[0].component, 'Header');
124
- assert.deepEqual(em.staticEmittersOf('unknown:event'), []);
125
- });
126
-
127
- test('loadGraph ignores malformed input', () => {
128
- const em = new EventManager();
129
- em.loadGraph(null);
130
- em.loadGraph({ nope: true });
131
- assert.equal(em.graph, null);
132
- });
133
-
134
- test('undeclared usage is tracked for the debugger drift view', () => {
135
- warnings.length = 0;
136
- const em = new EventManager();
137
- em.register({ 'declared:ok': { payload: null } });
138
- em.emit('drifted:event');
139
- assert.ok(em.undeclared.has('drifted:event'), 'drift set records undeclared usage');
140
- assert.ok(!em.undeclared.has('declared:ok'), 'declared events are not drift');
141
- });
@@ -1,21 +0,0 @@
1
- // Node ESM resolve hook so the REAL Controller can load under `node --test`.
2
- //
3
- // Controller.js does `import components from '/Components/components.js'` — a
4
- // browser-absolute path that the dev server serves but node can't resolve. We
5
- // map only that exact specifier to a small fixture components map (just data,
6
- // the same shape the real components.js exports). Everything else resolves
7
- // normally, so the tests exercise the real Slice + Controller code, not mocks.
8
- // AppProbe lives in a CUSTOM category ('AppServices') whose type is 'Service' —
9
- // it must be allowed as a singleton (the type, not the category name, decides).
10
- const COMPONENTS = { Probe: 'Service', ModalProbe: 'Visual', AppProbe: 'AppServices' };
11
- const STUB = `export default ${JSON.stringify(COMPONENTS)};`;
12
-
13
- export async function resolve(specifier, context, nextResolve) {
14
- if (specifier === '/Components/components.js') {
15
- return {
16
- url: `data:text/javascript,${encodeURIComponent(STUB)}`,
17
- shortCircuit: true
18
- };
19
- }
20
- return nextResolve(specifier, context);
21
- }
@@ -1,131 +0,0 @@
1
- // Real-Controller tests for the dev-mode leak detector (findOrphans + criteria).
2
- // Loads the REAL Controller via the resolve hook (same pattern as
3
- // destroy-cascade.test.js) and drives it with a minimal fake DOM. Fake nodes are
4
- // made `instanceof HTMLElement` because findOrphans filters on that.
5
- import { register } from 'node:module';
6
- import test from 'node:test';
7
- import assert from 'node:assert/strict';
8
-
9
- globalThis.alert = () => {};
10
- register(new URL('./fixtures/real-runtime-loader.mjs', import.meta.url));
11
-
12
- globalThis.HTMLElement = class HTMLElement {};
13
- globalThis.slice = {
14
- logger: { logError() {}, logWarning() {}, logInfo() {} },
15
- events: null,
16
- };
17
-
18
- const { default: Controller } = await import('../Components/Structural/Controller/Controller.js');
19
-
20
- function mkEl(sliceId, children = []) {
21
- return Object.assign(Object.create(HTMLElement.prototype), {
22
- sliceId,
23
- tagName: 'SLICE-X',
24
- isConnected: true,
25
- parentComponent: null,
26
- _children: children,
27
- querySelectorAll() {
28
- const out = [];
29
- const walk = (n) => { for (const c of n._children) { out.push(c); walk(c); } };
30
- walk(this);
31
- return out;
32
- },
33
- setAttribute() {},
34
- remove() { this.isConnected = false; },
35
- });
36
- }
37
-
38
- function flatten(root) {
39
- const out = [root];
40
- for (const c of root._children) out.push(...flatten(c));
41
- return out;
42
- }
43
-
44
- function buildTree(controller, root) {
45
- for (const node of flatten(root)) controller.registerComponent(node);
46
- controller.registerComponentsRecursively(root);
47
- }
48
-
49
- test('a built-then-detached component (no destroy) IS flagged', () => {
50
- const c = new Controller();
51
- const comp = mkEl('leaky');
52
- buildTree(c, comp);
53
- comp.isConnected = false; // simulate innerHTML='' / remove() without destroyComponent
54
-
55
- const orphans = c.findOrphans();
56
- assert.equal(orphans.length, 1, 'one orphan');
57
- assert.equal(orphans[0].sliceId, 'leaky');
58
- assert.ok(orphans[0].retainPath.includes('Object(leaky)') || orphans[0].retainPath.length >= 1);
59
- });
60
-
61
- test('a connected component is NOT flagged', () => {
62
- const c = new Controller();
63
- const comp = mkEl('alive');
64
- buildTree(c, comp);
65
- assert.equal(c.findOrphans().length, 0);
66
- });
67
-
68
- test('__sliceCached components are excluded (Route/MultiRoute cache)', () => {
69
- const c = new Controller();
70
- const comp = mkEl('cachedView');
71
- buildTree(c, comp);
72
- comp.isConnected = false;
73
- comp.__sliceCached = true;
74
- assert.equal(c.findOrphans().length, 0);
75
- });
76
-
77
- test('router-managed route-* sliceIds are excluded', () => {
78
- const c = new Controller();
79
- const comp = mkEl('route-Home');
80
- buildTree(c, comp);
81
- comp.isConnected = false;
82
- assert.equal(c.findOrphans().length, 0);
83
- });
84
-
85
- test('ancestor de-dup: only the detached root is reported', () => {
86
- const c = new Controller();
87
- const child = mkEl('child');
88
- const parent = mkEl('parent', [child]);
89
- buildTree(c, parent);
90
- parent.isConnected = false;
91
- child.isConnected = false;
92
-
93
- const orphans = c.findOrphans();
94
- assert.equal(orphans.length, 1, 'only the root, not the descendant');
95
- assert.equal(orphans[0].sliceId, 'parent');
96
- });
97
-
98
- test('descendants of a cached subtree are NOT flagged', () => {
99
- const c = new Controller();
100
- const child = mkEl('child');
101
- const parent = mkEl('parent', [child]);
102
- buildTree(c, parent);
103
- parent.__sliceCached = true; // cached route root
104
- parent.isConnected = false;
105
- child.isConnected = false;
106
- assert.equal(c.findOrphans().length, 0);
107
- });
108
-
109
- test('app-registered exclusion predicate is honored', () => {
110
- const c = new Controller();
111
- const comp = mkEl('special');
112
- buildTree(c, comp);
113
- comp.isConnected = false;
114
- c.registerLeakExclusion((node) => node.sliceId === 'special');
115
- assert.equal(c.findOrphans().length, 0);
116
- });
117
-
118
- test('growth heuristic trips on monotonic increase and resets on shrink', () => {
119
- const c = new Controller();
120
- assert.equal(c.isGrowthMonotonic(), false, 'needs >= 5 samples');
121
-
122
- for (let i = 0; i < 5; i++) {
123
- c.registerComponent(mkEl(`g${i}`));
124
- c.sampleComponentGrowth();
125
- }
126
- assert.equal(c.isGrowthMonotonic(), true, 'sizes 1..5 are monotonic growth');
127
-
128
- c.destroyComponent('g4');
129
- c.sampleComponentGrowth(); // size dips
130
- assert.equal(c.isGrowthMonotonic(), false, 'a shrink breaks the monotonic signal');
131
- });