slicejs-web-framework 4.0.0 → 4.0.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.
- package/.opencode/opencode.json +14 -14
- package/LICENSE +21 -21
- package/README.md +97 -97
- package/Slice/Components/Structural/ContextManager/ContextManager.js +422 -422
- package/Slice/Components/Structural/ContextManager/ContextManagerDebugger.js +470 -470
- package/Slice/Components/Structural/Controller/Controller.js +1406 -1406
- package/Slice/Components/Structural/Controller/allowedValuesValidation.js +52 -52
- package/Slice/Components/Structural/Debugger/Debugger.html +72 -72
- package/Slice/Components/Structural/Debugger/Debugger.js +1505 -1505
- package/Slice/Components/Structural/EventManager/EventManager.js +651 -651
- package/Slice/Components/Structural/EventManager/EventManagerDebugger.js +985 -985
- package/Slice/Components/Structural/LeakInspector/LeakInspector.js +296 -296
- package/Slice/Components/Structural/Logger/Log.js +10 -10
- package/Slice/Components/Structural/Logger/LogViewer/LogViewer.js +662 -662
- package/Slice/Components/Structural/Logger/Logger.js +225 -225
- package/Slice/Components/Structural/Router/Router.js +793 -793
- package/Slice/Components/Structural/StylesManager/StylesManager.js +82 -82
- package/Slice/Components/Structural/StylesManager/ThemeManager/ThemeManager.js +162 -162
- package/Slice/Slice.js +686 -697
- package/Slice/tests/build-bundled-component-without-category.test.js +103 -103
- package/Slice/tests/build-js-only-visual-components.test.js +144 -144
- package/Slice/tests/build-singleton.test.js +244 -244
- package/Slice/tests/bundle-v2-runtime-contract.test.js +738 -738
- package/Slice/tests/context-patch-use.test.js +95 -95
- package/Slice/tests/destroy-cascade.test.js +215 -215
- package/Slice/tests/event-emitters.test.js +79 -79
- package/Slice/tests/event-registry.test.js +141 -141
- package/Slice/tests/fixtures/real-runtime-loader.mjs +21 -21
- package/Slice/tests/leak-orphans.test.js +131 -131
- package/Slice/tests/props-allowed-values-validation.test.js +119 -119
- package/Slice/tests/public-env-runtime-accessors.test.js +44 -44
- package/Slice/tests/public-env-typed-accessors.test.js +66 -66
- package/Slice/tests/router-loading-finally.test.js +68 -68
- package/api/framework/devDepsOptimizer.js +335 -335
- package/api/framework/express.js +3 -3
- package/api/framework/precompressedStatic.js +93 -93
- package/api/framework/publicEnvResolver.js +123 -123
- package/api/framework/securityMiddleware.js +197 -197
- package/api/framework/server.js +411 -411
- package/api/index.js +7 -7
- package/api/tests/dev-deps-optimizer.test.js +137 -137
- package/api/tests/dev-server-external.test.js +56 -56
- package/api/tests/precompressed-static.test.js +119 -119
- package/api/tests/public-env-resolver.test.js +193 -193
- package/package.json +49 -49
- package/sliceConfig.schema.json +122 -122
- package/src/App/index.html +69 -69
- package/src/App/index.js +20 -20
- package/src/App/style.css +11 -11
- package/src/Components/AppComponents/AboutSection/AboutSection.css +9 -9
- package/src/Components/AppComponents/AboutSection/AboutSection.html +8 -8
- package/src/Components/AppComponents/AboutSection/AboutSection.js +12 -12
- package/src/Components/AppComponents/AppShell/AppShell.css +10 -10
- package/src/Components/AppComponents/AppShell/AppShell.html +4 -4
- package/src/Components/AppComponents/AppShell/AppShell.js +36 -36
- package/src/Components/AppComponents/HomeSection/HomeSection.css +20 -20
- package/src/Components/AppComponents/HomeSection/HomeSection.html +10 -10
- package/src/Components/AppComponents/HomeSection/HomeSection.js +19 -19
- package/src/Components/components.js +8 -8
- package/src/public/Styles/sliceStyles.css +34 -34
- package/src/public/Themes/Dark.css +42 -42
- package/src/public/Themes/Light.css +31 -31
- package/src/public/Themes/Slice.css +47 -47
- package/src/public/manifest.json +15 -15
- package/src/public/robots.txt +4 -4
- package/src/public/sitemap.xml +8 -8
- package/src/routes.js +9 -9
- package/src/sliceConfig.json +72 -72
- package/tests/router-emit-ordering.test.js +63 -63
- package/types/index.d.ts +219 -219
|
@@ -1,79 +1,79 @@
|
|
|
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
|
+
// 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 +1,141 @@
|
|
|
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
|
+
// 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 +1,21 @@
|
|
|
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
|
+
// 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
|
+
}
|