slicejs-web-framework 3.3.7 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,244 +1,244 @@
1
- // Real-runtime integration tests for build({ singleton: true }).
2
- //
3
- // These load the REAL Slice + REAL Controller (no FakeController): the only
4
- // fixture is the components map, injected via a resolve hook because Controller
5
- // imports a browser-absolute '/Components/components.js'. Singletons are
6
- // Service-only (no DOM), so the full build → getOrCreate → _build →
7
- // registerComponent path runs end-to-end under node, exercising the actual
8
- // shipped code.
9
- import { register } from 'node:module';
10
- import test from 'node:test';
11
- import assert from 'node:assert/strict';
12
-
13
- globalThis.alert = () => {};
14
- register(new URL('./fixtures/real-runtime-loader.mjs', import.meta.url));
15
-
16
- const { default: Slice } = await import('../Slice.js');
17
- const { default: Controller } = await import('../Components/Structural/Controller/Controller.js');
18
-
19
- // A real Service-shaped class. The components fixture registers 'Probe' as a
20
- // Service and 'ModalProbe' as a Visual.
21
- let constructCount = 0;
22
- let failNext = false;
23
-
24
- class Probe {
25
- constructor(props) {
26
- constructCount += 1;
27
- if (failNext) {
28
- failNext = false;
29
- throw new Error('boom'); // simulate a failed build on first attempt
30
- }
31
- this.props = props;
32
- }
33
- async init() {}
34
- }
35
-
36
- class FakeStylesManager {
37
- registerComponentStyles() {}
38
- }
39
-
40
- function createSlice() {
41
- const instance = new Slice(
42
- {
43
- paths: {
44
- components: {
45
- Service: { path: '/Components/Service', type: 'Service' },
46
- Visual: { path: '/Components/Visual', type: 'Visual' },
47
- // Custom category whose type is Service (e.g. an app's own services).
48
- AppServices: { path: '/Components/AppServices', type: 'Service' }
49
- }
50
- },
51
- themeManager: {},
52
- stylesManager: {},
53
- logger: {},
54
- debugger: { enabled: false },
55
- loading: {},
56
- events: {}
57
- },
58
- { Controller, StylesManager: FakeStylesManager }
59
- );
60
-
61
- instance.logger = { logError() {}, logWarning() {}, logInfo() {} };
62
- // Pre-seed the class so _build skips the network fetch (the real path for a
63
- // class already cached by the controller). Everything else is real.
64
- instance.controller.classes.set('Probe', Probe);
65
- // AppProbe is a Service-shaped class registered under the custom 'AppServices'
66
- // category; reuse the Probe class (same shape).
67
- instance.controller.classes.set('AppProbe', Probe);
68
- return instance;
69
- }
70
-
71
- function withSlice(fn) {
72
- return async () => {
73
- const original = globalThis.slice;
74
- constructCount = 0;
75
- failNext = false;
76
- const slice = createSlice();
77
- globalThis.slice = slice;
78
- try {
79
- await fn(slice);
80
- } finally {
81
- globalThis.slice = original;
82
- }
83
- };
84
- }
85
-
86
- test(
87
- 'singleton returns the same instance across awaited calls and builds once',
88
- withSlice(async (slice) => {
89
- const a = await slice.build('Probe', { singleton: true });
90
- const b = await slice.build('Probe', { singleton: true });
91
-
92
- assert.ok(a, 'first call returns an instance');
93
- assert.equal(a, b, 'same instance reference');
94
- assert.equal(constructCount, 1, 'constructed exactly once');
95
- assert.equal(a.sliceId, 'Probe', 'sliceId defaults to component name');
96
- assert.equal(slice.controller.activeComponents.get('Probe'), a, 'registered in activeComponents');
97
- })
98
- );
99
-
100
- test(
101
- 'concurrent singleton builds share one in-flight build (race-safe)',
102
- withSlice(async (slice) => {
103
- const [a, b] = await Promise.all([
104
- slice.build('Probe', { singleton: true }),
105
- slice.build('Probe', { singleton: true })
106
- ]);
107
-
108
- assert.equal(a, b, 'both callers get the same instance');
109
- assert.equal(constructCount, 1, 'deduped to a single construction');
110
- })
111
- );
112
-
113
- test(
114
- 'named singletons via distinct sliceId are separate instances',
115
- withSlice(async (slice) => {
116
- const a = await slice.build('Probe', { singleton: true, sliceId: 'probeA' });
117
- const b = await slice.build('Probe', { singleton: true, sliceId: 'probeB' });
118
-
119
- assert.notEqual(a, b);
120
- assert.equal(a.sliceId, 'probeA');
121
- assert.equal(b.sliceId, 'probeB');
122
- assert.equal(constructCount, 2);
123
- })
124
- );
125
-
126
- test(
127
- 'a failed singleton build is not cached and the next call retries',
128
- withSlice(async (slice) => {
129
- failNext = true;
130
- const first = await slice.build('Probe', { singleton: true });
131
- assert.equal(first, null, 'failed build resolves to null');
132
- assert.equal(
133
- slice.controller._pendingBuilds.has('Probe'),
134
- false,
135
- 'pending entry cleared after settle'
136
- );
137
- assert.equal(
138
- slice.controller.activeComponents.has('Probe'),
139
- false,
140
- 'failed build never registered'
141
- );
142
-
143
- const second = await slice.build('Probe', { singleton: true });
144
- assert.ok(second, 'retry succeeds');
145
- assert.equal(constructCount, 2, 'retried construction');
146
- })
147
- );
148
-
149
- test(
150
- 'singleton:true is rejected for non-Service components',
151
- withSlice(async (slice) => {
152
- let errored = false;
153
- slice.logger.logError = () => {
154
- errored = true;
155
- };
156
-
157
- const result = await slice.build('ModalProbe', { singleton: true });
158
-
159
- assert.equal(result, null, 'returns null for a Visual');
160
- assert.equal(errored, true, 'logs an error');
161
- assert.equal(constructCount, 0, 'never constructed');
162
- })
163
- );
164
-
165
- test(
166
- 'singleton:true is allowed for a custom Service-type category (AppServices)',
167
- withSlice(async (slice) => {
168
- let errored = false;
169
- slice.logger.logError = () => {
170
- errored = true;
171
- };
172
-
173
- const a = await slice.build('AppProbe', { singleton: true });
174
- const b = await slice.build('AppProbe', { singleton: true });
175
-
176
- assert.ok(a, 'builds the instance');
177
- assert.equal(a, b, 'returns the same singleton instance');
178
- assert.equal(errored, false, 'does not log an error for a Service-type category');
179
- assert.equal(constructCount, 1, 'constructed exactly once');
180
- })
181
- );
182
-
183
- test(
184
- 'default (non-singleton) build path is unchanged',
185
- withSlice(async (slice) => {
186
- const a = await slice.build('Probe');
187
- const b = await slice.build('Probe');
188
-
189
- assert.ok(a);
190
- assert.ok(b);
191
- assert.notEqual(a, b, 'each non-singleton build is a fresh instance');
192
- assert.equal(constructCount, 2);
193
- })
194
- );
195
-
196
- test(
197
- 'props only apply on the first (creating) call',
198
- withSlice(async (slice) => {
199
- const a = await slice.build('Probe', { singleton: true, tag: 'first' });
200
- const b = await slice.build('Probe', { singleton: true, tag: 'second' });
201
-
202
- assert.equal(a, b, 'same instance');
203
- assert.equal(constructCount, 1, 'not rebuilt');
204
- assert.equal(a.props.tag, 'first', 'later props are ignored');
205
- })
206
- );
207
-
208
- test(
209
- 'the singleton/sliceId directives are not forwarded to the component props',
210
- withSlice(async (slice) => {
211
- const a = await slice.build('Probe', { singleton: true, sliceId: 'probeX', tag: 'keep' });
212
-
213
- assert.equal(a.props.tag, 'keep', 'real props pass through');
214
- assert.equal('singleton' in a.props, false, 'singleton flag stripped');
215
- assert.equal('sliceId' in a.props, false, 'sliceId directive stripped');
216
- })
217
- );
218
-
219
- test(
220
- 'singleton is stripped from props even on a non-singleton build',
221
- withSlice(async (slice) => {
222
- const a = await slice.build('Probe', { singleton: false, tag: 'keep' });
223
-
224
- assert.ok(a);
225
- assert.equal(a.props.tag, 'keep', 'real props pass through');
226
- assert.equal('singleton' in a.props, false, 'singleton key never reaches the component');
227
- })
228
- );
229
-
230
- test(
231
- 'singleton:true on an unregistered component is rejected (unknown category)',
232
- withSlice(async (slice) => {
233
- let errored = false;
234
- slice.logger.logError = () => {
235
- errored = true;
236
- };
237
-
238
- const result = await slice.build('NotRegistered', { singleton: true });
239
-
240
- assert.equal(result, null, 'returns null');
241
- assert.equal(errored, true, 'logs an error');
242
- assert.equal(constructCount, 0, 'never constructed');
243
- })
244
- );
1
+ // Real-runtime integration tests for build({ singleton: true }).
2
+ //
3
+ // These load the REAL Slice + REAL Controller (no FakeController): the only
4
+ // fixture is the components map, injected via a resolve hook because Controller
5
+ // imports a browser-absolute '/Components/components.js'. Singletons are
6
+ // Service-only (no DOM), so the full build → getOrCreate → _build →
7
+ // registerComponent path runs end-to-end under node, exercising the actual
8
+ // shipped code.
9
+ import { register } from 'node:module';
10
+ import test from 'node:test';
11
+ import assert from 'node:assert/strict';
12
+
13
+ globalThis.alert = () => {};
14
+ register(new URL('./fixtures/real-runtime-loader.mjs', import.meta.url));
15
+
16
+ const { default: Slice } = await import('../Slice.js');
17
+ const { default: Controller } = await import('../Components/Structural/Controller/Controller.js');
18
+
19
+ // A real Service-shaped class. The components fixture registers 'Probe' as a
20
+ // Service and 'ModalProbe' as a Visual.
21
+ let constructCount = 0;
22
+ let failNext = false;
23
+
24
+ class Probe {
25
+ constructor(props) {
26
+ constructCount += 1;
27
+ if (failNext) {
28
+ failNext = false;
29
+ throw new Error('boom'); // simulate a failed build on first attempt
30
+ }
31
+ this.props = props;
32
+ }
33
+ async init() {}
34
+ }
35
+
36
+ class FakeStylesManager {
37
+ registerComponentStyles() {}
38
+ }
39
+
40
+ function createSlice() {
41
+ const instance = new Slice(
42
+ {
43
+ paths: {
44
+ components: {
45
+ Service: { path: '/Components/Service', type: 'Service' },
46
+ Visual: { path: '/Components/Visual', type: 'Visual' },
47
+ // Custom category whose type is Service (e.g. an app's own services).
48
+ AppServices: { path: '/Components/AppServices', type: 'Service' }
49
+ }
50
+ },
51
+ themeManager: {},
52
+ stylesManager: {},
53
+ logger: {},
54
+ debugger: { enabled: false },
55
+ loading: {},
56
+ events: {}
57
+ },
58
+ { Controller, StylesManager: FakeStylesManager }
59
+ );
60
+
61
+ instance.logger = { logError() {}, logWarning() {}, logInfo() {} };
62
+ // Pre-seed the class so _build skips the network fetch (the real path for a
63
+ // class already cached by the controller). Everything else is real.
64
+ instance.controller.classes.set('Probe', Probe);
65
+ // AppProbe is a Service-shaped class registered under the custom 'AppServices'
66
+ // category; reuse the Probe class (same shape).
67
+ instance.controller.classes.set('AppProbe', Probe);
68
+ return instance;
69
+ }
70
+
71
+ function withSlice(fn) {
72
+ return async () => {
73
+ const original = globalThis.slice;
74
+ constructCount = 0;
75
+ failNext = false;
76
+ const slice = createSlice();
77
+ globalThis.slice = slice;
78
+ try {
79
+ await fn(slice);
80
+ } finally {
81
+ globalThis.slice = original;
82
+ }
83
+ };
84
+ }
85
+
86
+ test(
87
+ 'singleton returns the same instance across awaited calls and builds once',
88
+ withSlice(async (slice) => {
89
+ const a = await slice.build('Probe', { singleton: true });
90
+ const b = await slice.build('Probe', { singleton: true });
91
+
92
+ assert.ok(a, 'first call returns an instance');
93
+ assert.equal(a, b, 'same instance reference');
94
+ assert.equal(constructCount, 1, 'constructed exactly once');
95
+ assert.equal(a.sliceId, 'Probe', 'sliceId defaults to component name');
96
+ assert.equal(slice.controller.activeComponents.get('Probe'), a, 'registered in activeComponents');
97
+ })
98
+ );
99
+
100
+ test(
101
+ 'concurrent singleton builds share one in-flight build (race-safe)',
102
+ withSlice(async (slice) => {
103
+ const [a, b] = await Promise.all([
104
+ slice.build('Probe', { singleton: true }),
105
+ slice.build('Probe', { singleton: true })
106
+ ]);
107
+
108
+ assert.equal(a, b, 'both callers get the same instance');
109
+ assert.equal(constructCount, 1, 'deduped to a single construction');
110
+ })
111
+ );
112
+
113
+ test(
114
+ 'named singletons via distinct sliceId are separate instances',
115
+ withSlice(async (slice) => {
116
+ const a = await slice.build('Probe', { singleton: true, sliceId: 'probeA' });
117
+ const b = await slice.build('Probe', { singleton: true, sliceId: 'probeB' });
118
+
119
+ assert.notEqual(a, b);
120
+ assert.equal(a.sliceId, 'probeA');
121
+ assert.equal(b.sliceId, 'probeB');
122
+ assert.equal(constructCount, 2);
123
+ })
124
+ );
125
+
126
+ test(
127
+ 'a failed singleton build is not cached and the next call retries',
128
+ withSlice(async (slice) => {
129
+ failNext = true;
130
+ const first = await slice.build('Probe', { singleton: true });
131
+ assert.equal(first, null, 'failed build resolves to null');
132
+ assert.equal(
133
+ slice.controller._pendingBuilds.has('Probe'),
134
+ false,
135
+ 'pending entry cleared after settle'
136
+ );
137
+ assert.equal(
138
+ slice.controller.activeComponents.has('Probe'),
139
+ false,
140
+ 'failed build never registered'
141
+ );
142
+
143
+ const second = await slice.build('Probe', { singleton: true });
144
+ assert.ok(second, 'retry succeeds');
145
+ assert.equal(constructCount, 2, 'retried construction');
146
+ })
147
+ );
148
+
149
+ test(
150
+ 'singleton:true is rejected for non-Service components',
151
+ withSlice(async (slice) => {
152
+ let errored = false;
153
+ slice.logger.logError = () => {
154
+ errored = true;
155
+ };
156
+
157
+ const result = await slice.build('ModalProbe', { singleton: true });
158
+
159
+ assert.equal(result, null, 'returns null for a Visual');
160
+ assert.equal(errored, true, 'logs an error');
161
+ assert.equal(constructCount, 0, 'never constructed');
162
+ })
163
+ );
164
+
165
+ test(
166
+ 'singleton:true is allowed for a custom Service-type category (AppServices)',
167
+ withSlice(async (slice) => {
168
+ let errored = false;
169
+ slice.logger.logError = () => {
170
+ errored = true;
171
+ };
172
+
173
+ const a = await slice.build('AppProbe', { singleton: true });
174
+ const b = await slice.build('AppProbe', { singleton: true });
175
+
176
+ assert.ok(a, 'builds the instance');
177
+ assert.equal(a, b, 'returns the same singleton instance');
178
+ assert.equal(errored, false, 'does not log an error for a Service-type category');
179
+ assert.equal(constructCount, 1, 'constructed exactly once');
180
+ })
181
+ );
182
+
183
+ test(
184
+ 'default (non-singleton) build path is unchanged',
185
+ withSlice(async (slice) => {
186
+ const a = await slice.build('Probe');
187
+ const b = await slice.build('Probe');
188
+
189
+ assert.ok(a);
190
+ assert.ok(b);
191
+ assert.notEqual(a, b, 'each non-singleton build is a fresh instance');
192
+ assert.equal(constructCount, 2);
193
+ })
194
+ );
195
+
196
+ test(
197
+ 'props only apply on the first (creating) call',
198
+ withSlice(async (slice) => {
199
+ const a = await slice.build('Probe', { singleton: true, tag: 'first' });
200
+ const b = await slice.build('Probe', { singleton: true, tag: 'second' });
201
+
202
+ assert.equal(a, b, 'same instance');
203
+ assert.equal(constructCount, 1, 'not rebuilt');
204
+ assert.equal(a.props.tag, 'first', 'later props are ignored');
205
+ })
206
+ );
207
+
208
+ test(
209
+ 'the singleton/sliceId directives are not forwarded to the component props',
210
+ withSlice(async (slice) => {
211
+ const a = await slice.build('Probe', { singleton: true, sliceId: 'probeX', tag: 'keep' });
212
+
213
+ assert.equal(a.props.tag, 'keep', 'real props pass through');
214
+ assert.equal('singleton' in a.props, false, 'singleton flag stripped');
215
+ assert.equal('sliceId' in a.props, false, 'sliceId directive stripped');
216
+ })
217
+ );
218
+
219
+ test(
220
+ 'singleton is stripped from props even on a non-singleton build',
221
+ withSlice(async (slice) => {
222
+ const a = await slice.build('Probe', { singleton: false, tag: 'keep' });
223
+
224
+ assert.ok(a);
225
+ assert.equal(a.props.tag, 'keep', 'real props pass through');
226
+ assert.equal('singleton' in a.props, false, 'singleton key never reaches the component');
227
+ })
228
+ );
229
+
230
+ test(
231
+ 'singleton:true on an unregistered component is rejected (unknown category)',
232
+ withSlice(async (slice) => {
233
+ let errored = false;
234
+ slice.logger.logError = () => {
235
+ errored = true;
236
+ };
237
+
238
+ const result = await slice.build('NotRegistered', { singleton: true });
239
+
240
+ assert.equal(result, null, 'returns null');
241
+ assert.equal(errored, true, 'logs an error');
242
+ assert.equal(constructCount, 0, 'never constructed');
243
+ })
244
+ );