@mmapp/react 0.1.0-alpha.1 → 0.1.0-alpha.3
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/README.md +112 -0
- package/dist/index.d.mts +27 -2
- package/dist/index.d.ts +27 -2
- package/dist/index.js +70 -3
- package/dist/index.mjs +74 -12
- package/package.json +4 -3
- package/package.json.backup +0 -41
- package/src/Blueprint.ts +0 -216
- package/src/__tests__/Blueprint.test.ts +0 -106
- package/src/__tests__/action-context.test.ts +0 -166
- package/src/__tests__/actionCreators.test.ts +0 -179
- package/src/__tests__/builders.test.ts +0 -336
- package/src/__tests__/defineBlueprint-composition.test.ts +0 -106
- package/src/__tests__/factories.test.ts +0 -229
- package/src/__tests__/loader.test.ts +0 -159
- package/src/__tests__/logger.test.ts +0 -70
- package/src/__tests__/type-inference.test.ts +0 -160
- package/src/__tests__/typed-transitions.test.ts +0 -126
- package/src/__tests__/useModuleConfig.test.ts +0 -61
- package/src/actionCreators.ts +0 -132
- package/src/actions.ts +0 -547
- package/src/atoms/index.ts +0 -600
- package/src/authoring.ts +0 -92
- package/src/browser-player.ts +0 -783
- package/src/builders.ts +0 -1342
- package/src/components/ExperienceWorkflowBridge.tsx +0 -123
- package/src/components/PlayerProvider.tsx +0 -43
- package/src/components/atoms/index.tsx +0 -269
- package/src/components/index.ts +0 -36
- package/src/conditions.ts +0 -692
- package/src/config/defineBlueprint.ts +0 -329
- package/src/config/defineModel.ts +0 -753
- package/src/config/defineWorkspace.ts +0 -24
- package/src/core/WorkflowRuntime.ts +0 -153
- package/src/factories.ts +0 -425
- package/src/grammar/index.ts +0 -173
- package/src/hooks/index.ts +0 -106
- package/src/hooks/useAuth.ts +0 -288
- package/src/hooks/useChannel.ts +0 -304
- package/src/hooks/useComputed.ts +0 -154
- package/src/hooks/useDomainSubscription.ts +0 -110
- package/src/hooks/useDuringAction.ts +0 -99
- package/src/hooks/useExperienceState.ts +0 -59
- package/src/hooks/useExpressionLibrary.ts +0 -129
- package/src/hooks/useForm.ts +0 -352
- package/src/hooks/useGeolocation.ts +0 -207
- package/src/hooks/useMapView.ts +0 -259
- package/src/hooks/useMiddleware.ts +0 -291
- package/src/hooks/useModel.ts +0 -363
- package/src/hooks/useModule.ts +0 -59
- package/src/hooks/useModuleConfig.ts +0 -61
- package/src/hooks/useMutation.ts +0 -237
- package/src/hooks/useNotification.ts +0 -151
- package/src/hooks/useOnChange.ts +0 -30
- package/src/hooks/useOnEnter.ts +0 -59
- package/src/hooks/useOnEvent.ts +0 -37
- package/src/hooks/useOnExit.ts +0 -27
- package/src/hooks/useOnTransition.ts +0 -30
- package/src/hooks/usePackage.ts +0 -128
- package/src/hooks/useParams.ts +0 -33
- package/src/hooks/usePlayer.ts +0 -308
- package/src/hooks/useQuery.ts +0 -184
- package/src/hooks/useRealtimeQuery.ts +0 -222
- package/src/hooks/useRole.ts +0 -191
- package/src/hooks/useRouteParams.ts +0 -100
- package/src/hooks/useRouter.ts +0 -347
- package/src/hooks/useServerAction.ts +0 -178
- package/src/hooks/useServerState.ts +0 -284
- package/src/hooks/useToast.ts +0 -164
- package/src/hooks/useTransition.ts +0 -39
- package/src/hooks/useView.ts +0 -102
- package/src/hooks/useWhileIn.ts +0 -48
- package/src/hooks/useWorkflow.ts +0 -63
- package/src/index.ts +0 -465
- package/src/loader/experience-workflow-loader.ts +0 -192
- package/src/loader/index.ts +0 -6
- package/src/local/LocalEngine.ts +0 -388
- package/src/local/LocalEngineAdapter.ts +0 -175
- package/src/local/LocalEngineContext.ts +0 -30
- package/src/logger.ts +0 -37
- package/src/mixins.ts +0 -1160
- package/src/providers/RuntimeContext.ts +0 -20
- package/src/providers/WorkflowProvider.tsx +0 -28
- package/src/routing/instance-key.ts +0 -107
- package/src/server/transition-context.ts +0 -172
- package/src/testing/index.ts +0 -9
- package/src/testing/useBlueprintTestRunner.ts +0 -91
- package/src/testing/useGraphAnalysis.ts +0 -18
- package/src/testing/useTestRunner.ts +0 -77
- package/src/testing.ts +0 -995
- package/src/types/workflow-inference.ts +0 -158
- package/src/types.ts +0 -114
- package/tsconfig.json +0 -27
- package/vitest.config.ts +0 -8
|
@@ -1,336 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { model, field, state, transition, StateBuilder } from '../builders';
|
|
3
|
-
import { setField, logEvent, serverAction } from '../actions';
|
|
4
|
-
|
|
5
|
-
describe('StateBuilder.to() — inline transitions', () => {
|
|
6
|
-
it('declares a single outgoing transition', () => {
|
|
7
|
-
const result = model('test')
|
|
8
|
-
.state('draft', state.initial().to('review', 'submit'))
|
|
9
|
-
.state('review')
|
|
10
|
-
.build();
|
|
11
|
-
|
|
12
|
-
expect(result.transitions.submit).toBeDefined();
|
|
13
|
-
expect(result.transitions.submit.from).toBe('draft');
|
|
14
|
-
expect(result.transitions.submit.to).toBe('review');
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it('declares multiple outgoing transitions from one state', () => {
|
|
18
|
-
const result = model('test')
|
|
19
|
-
.state('review', new StateBuilder()
|
|
20
|
-
.to('approved', 'approve')
|
|
21
|
-
.to('rejected', 'reject')
|
|
22
|
-
)
|
|
23
|
-
.build();
|
|
24
|
-
|
|
25
|
-
expect(result.transitions.approve).toEqual({ from: 'review', to: 'approved' });
|
|
26
|
-
expect(result.transitions.reject).toEqual({ from: 'review', to: 'rejected' });
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('supports transition configuration via callback', () => {
|
|
30
|
-
const result = model('test')
|
|
31
|
-
.state('draft', state.initial()
|
|
32
|
-
.to('review', 'submit', t => t.require('title', 'body').roles('author'))
|
|
33
|
-
)
|
|
34
|
-
.state('review')
|
|
35
|
-
.build();
|
|
36
|
-
|
|
37
|
-
expect(result.transitions.submit.requiredFields).toEqual(['title', 'body']);
|
|
38
|
-
expect(result.transitions.submit.roles).toEqual(['author']);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('standalone transitions override inline transitions', () => {
|
|
42
|
-
const result = model('test')
|
|
43
|
-
.state('draft', state.initial().to('review', 'submit'))
|
|
44
|
-
.state('review')
|
|
45
|
-
.transition('submit', transition.from('draft').to('review').roles('admin'))
|
|
46
|
-
.build();
|
|
47
|
-
|
|
48
|
-
expect(result.transitions.submit.roles).toEqual(['admin']);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('inline transitions work with actions', () => {
|
|
52
|
-
const result = model('test')
|
|
53
|
-
.state('active', new StateBuilder()
|
|
54
|
-
.to('inactive', 'deactivate', t =>
|
|
55
|
-
t.do(setField('active', 'false'), logEvent('deactivated'))
|
|
56
|
-
)
|
|
57
|
-
)
|
|
58
|
-
.build();
|
|
59
|
-
|
|
60
|
-
expect(result.transitions.deactivate.actions).toHaveLength(2);
|
|
61
|
-
expect(result.transitions.deactivate.actions![0].type).toBe('set_field');
|
|
62
|
-
expect(result.transitions.deactivate.actions![1].type).toBe('log_event');
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('coexists with standalone transitions on the same model', () => {
|
|
66
|
-
const result = model('test')
|
|
67
|
-
.state('draft', state.initial().to('review', 'submit'))
|
|
68
|
-
.state('review')
|
|
69
|
-
.state('published')
|
|
70
|
-
.transition('publish', transition.from('review').to('published'))
|
|
71
|
-
.build();
|
|
72
|
-
|
|
73
|
-
expect(result.transitions.submit.from).toBe('draft');
|
|
74
|
-
expect(result.transitions.publish.from).toBe('review');
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
describe('ModelBuilder.line() — auto-named transitions', () => {
|
|
79
|
-
it('auto-generates transition names from state pairs', () => {
|
|
80
|
-
const result = model('order')
|
|
81
|
-
.line(
|
|
82
|
-
['placed', state.initial()],
|
|
83
|
-
'confirmed',
|
|
84
|
-
'shipped',
|
|
85
|
-
['delivered', state.end()],
|
|
86
|
-
)
|
|
87
|
-
.build();
|
|
88
|
-
|
|
89
|
-
expect(Object.keys(result.states).sort()).toEqual(
|
|
90
|
-
['confirmed', 'delivered', 'placed', 'shipped'].sort()
|
|
91
|
-
);
|
|
92
|
-
expect(result.states.placed.type).toBe('initial');
|
|
93
|
-
expect(result.states.delivered.type).toBe('end');
|
|
94
|
-
expect(result.transitions.placed_to_confirmed).toEqual({ from: 'placed', to: 'confirmed' });
|
|
95
|
-
expect(result.transitions.confirmed_to_shipped).toEqual({ from: 'confirmed', to: 'shipped' });
|
|
96
|
-
expect(result.transitions.shipped_to_delivered).toEqual({ from: 'shipped', to: 'delivered' });
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('configures incoming edge via tuple callback', () => {
|
|
100
|
-
const result = model('test')
|
|
101
|
-
.line(
|
|
102
|
-
'draft',
|
|
103
|
-
['review', t => t.require('title', 'body')],
|
|
104
|
-
['approved', t => t.roles('manager')],
|
|
105
|
-
)
|
|
106
|
-
.build();
|
|
107
|
-
|
|
108
|
-
expect(result.transitions.draft_to_review.requiredFields).toEqual(['title', 'body']);
|
|
109
|
-
expect(result.transitions.review_to_approved.roles).toEqual(['manager']);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it('branching via multiple lines', () => {
|
|
113
|
-
const result = model('order')
|
|
114
|
-
.line(
|
|
115
|
-
['placed', state.initial()],
|
|
116
|
-
'confirmed',
|
|
117
|
-
'shipped',
|
|
118
|
-
)
|
|
119
|
-
.line('placed', 'cancelled')
|
|
120
|
-
.line('confirmed', 'cancelled')
|
|
121
|
-
.build();
|
|
122
|
-
|
|
123
|
-
expect(Object.keys(result.transitions).sort()).toEqual([
|
|
124
|
-
'confirmed_to_cancelled',
|
|
125
|
-
'confirmed_to_shipped',
|
|
126
|
-
'placed_to_cancelled',
|
|
127
|
-
'placed_to_confirmed',
|
|
128
|
-
].sort());
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it('two-state line', () => {
|
|
132
|
-
const result = model('test')
|
|
133
|
-
.line('active', 'inactive')
|
|
134
|
-
.build();
|
|
135
|
-
|
|
136
|
-
expect(result.transitions.active_to_inactive).toEqual({ from: 'active', to: 'inactive' });
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('self-loop', () => {
|
|
140
|
-
const result = model('test')
|
|
141
|
-
.line('active', 'active')
|
|
142
|
-
.build();
|
|
143
|
-
|
|
144
|
-
expect(result.transitions.active_to_active).toEqual({ from: 'active', to: 'active' });
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('with actions on edges', () => {
|
|
148
|
-
const result = model('test')
|
|
149
|
-
.line(
|
|
150
|
-
'draft',
|
|
151
|
-
['published', t => t.do(logEvent('published'), setField('publishedAt', 'NOW()'))],
|
|
152
|
-
)
|
|
153
|
-
.build();
|
|
154
|
-
|
|
155
|
-
const tr = result.transitions.draft_to_published;
|
|
156
|
-
expect(tr.actions).toHaveLength(2);
|
|
157
|
-
expect(tr.actions![0].type).toBe('log_event');
|
|
158
|
-
expect(tr.actions![1].type).toBe('set_field');
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it('works with field definitions', () => {
|
|
162
|
-
const result = model('invoice')
|
|
163
|
-
.field('amount', field.currency().required())
|
|
164
|
-
.line(
|
|
165
|
-
['draft', state.initial()],
|
|
166
|
-
['sent', t => t.require('amount')],
|
|
167
|
-
['paid', state.end()],
|
|
168
|
-
)
|
|
169
|
-
.build();
|
|
170
|
-
|
|
171
|
-
expect(result.fields.amount.type).toBe('currency');
|
|
172
|
-
expect(result.transitions.draft_to_sent.requiredFields).toEqual(['amount']);
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it('mixes with .state() and .transition() methods', () => {
|
|
176
|
-
const result = model('test')
|
|
177
|
-
.state('error', new StateBuilder().onEnter(setField('hasError', 'true')))
|
|
178
|
-
.line(
|
|
179
|
-
['draft', state.initial()],
|
|
180
|
-
'review',
|
|
181
|
-
['published', state.end()],
|
|
182
|
-
)
|
|
183
|
-
.transition('fail', transition.from('review').to('error'))
|
|
184
|
-
.build();
|
|
185
|
-
|
|
186
|
-
expect(result.states.error.onEnter).toHaveLength(1);
|
|
187
|
-
expect(result.states.draft.type).toBe('initial');
|
|
188
|
-
expect(result.transitions.draft_to_review).toBeDefined();
|
|
189
|
-
expect(result.transitions.fail.from).toBe('review');
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it('shared states across lines are unified', () => {
|
|
193
|
-
const result = model('auth')
|
|
194
|
-
.line(
|
|
195
|
-
['unauthenticated', state.initial()],
|
|
196
|
-
'authenticating',
|
|
197
|
-
'authenticated',
|
|
198
|
-
)
|
|
199
|
-
.line(
|
|
200
|
-
'unauthenticated',
|
|
201
|
-
'authenticating',
|
|
202
|
-
'authenticated',
|
|
203
|
-
)
|
|
204
|
-
.build();
|
|
205
|
-
|
|
206
|
-
// States appear only once
|
|
207
|
-
expect(Object.keys(result.states)).toEqual([
|
|
208
|
-
'unauthenticated', 'authenticating', 'authenticated',
|
|
209
|
-
]);
|
|
210
|
-
expect(result.states.unauthenticated.type).toBe('initial');
|
|
211
|
-
});
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
describe('ModelBuilder.line() — named transitions via tuple', () => {
|
|
215
|
-
it('overrides auto-name with explicit transition name', () => {
|
|
216
|
-
const result = model('test')
|
|
217
|
-
.line(
|
|
218
|
-
'draft',
|
|
219
|
-
['review', 'submit'],
|
|
220
|
-
['published', 'approve'],
|
|
221
|
-
)
|
|
222
|
-
.build();
|
|
223
|
-
|
|
224
|
-
expect(result.transitions.submit).toEqual({ from: 'draft', to: 'review' });
|
|
225
|
-
expect(result.transitions.approve).toEqual({ from: 'review', to: 'published' });
|
|
226
|
-
// Auto-named versions should NOT exist
|
|
227
|
-
expect(result.transitions.draft_to_review).toBeUndefined();
|
|
228
|
-
expect(result.transitions.review_to_published).toBeUndefined();
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
it('named transition with configuration', () => {
|
|
232
|
-
const result = model('test')
|
|
233
|
-
.line(
|
|
234
|
-
'draft',
|
|
235
|
-
['review', 'submit', t => t.require('title')],
|
|
236
|
-
['published', 'approve', t => t.roles('manager')],
|
|
237
|
-
)
|
|
238
|
-
.build();
|
|
239
|
-
|
|
240
|
-
expect(result.transitions.submit.requiredFields).toEqual(['title']);
|
|
241
|
-
expect(result.transitions.approve.roles).toEqual(['manager']);
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
it('mix named and auto-named in same line', () => {
|
|
245
|
-
const result = model('test')
|
|
246
|
-
.line(
|
|
247
|
-
'draft',
|
|
248
|
-
['review', 'submit'], // named
|
|
249
|
-
'published', // auto-named: review_to_published
|
|
250
|
-
)
|
|
251
|
-
.build();
|
|
252
|
-
|
|
253
|
-
expect(result.transitions.submit).toBeDefined();
|
|
254
|
-
expect(result.transitions.review_to_published).toBeDefined();
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
it('merges from-states for named transitions across lines', () => {
|
|
258
|
-
const result = model('order')
|
|
259
|
-
.line('placed', ['cancelled', 'cancel'])
|
|
260
|
-
.line('confirmed', ['cancelled', 'cancel'])
|
|
261
|
-
.build();
|
|
262
|
-
|
|
263
|
-
const cancelFrom = result.transitions.cancel.from;
|
|
264
|
-
expect(Array.isArray(cancelFrom)).toBe(true);
|
|
265
|
-
expect(cancelFrom).toContain('placed');
|
|
266
|
-
expect(cancelFrom).toContain('confirmed');
|
|
267
|
-
});
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
describe('ModelBuilder.line() — complete auth example', () => {
|
|
271
|
-
it('models auth with auto-named transitions', () => {
|
|
272
|
-
const result = model('mod-authentication')
|
|
273
|
-
.field('errorMessage', field.string(''))
|
|
274
|
-
|
|
275
|
-
// Login flow
|
|
276
|
-
.line(
|
|
277
|
-
['unauthenticated', state.initial().onEnter(setField('errorMessage', '""'))],
|
|
278
|
-
['authenticating', t => t.do(serverAction('authenticate', { method: 'login' }))],
|
|
279
|
-
'authenticated',
|
|
280
|
-
)
|
|
281
|
-
|
|
282
|
-
// Error handling
|
|
283
|
-
.line('authenticating', 'error')
|
|
284
|
-
.line('error', 'unauthenticated')
|
|
285
|
-
|
|
286
|
-
// Logout
|
|
287
|
-
.line(
|
|
288
|
-
'authenticated',
|
|
289
|
-
['unauthenticated', t => t.do(serverAction('destroy_session'))],
|
|
290
|
-
)
|
|
291
|
-
.build();
|
|
292
|
-
|
|
293
|
-
expect(result.states.unauthenticated.type).toBe('initial');
|
|
294
|
-
expect(result.transitions.unauthenticated_to_authenticating.actions![0].type).toBe('server:authenticate');
|
|
295
|
-
expect(result.transitions.authenticated_to_unauthenticated.actions![0].type).toBe('server:destroy_session');
|
|
296
|
-
expect(result.transitions.authenticating_to_error).toBeDefined();
|
|
297
|
-
expect(result.transitions.error_to_unauthenticated).toBeDefined();
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
it('models auth with named transitions', () => {
|
|
301
|
-
const result = model('mod-authentication')
|
|
302
|
-
.field('errorMessage', field.string(''))
|
|
303
|
-
|
|
304
|
-
.line(
|
|
305
|
-
['unauthenticated', state.initial().onEnter(setField('errorMessage', '""'))],
|
|
306
|
-
['authenticating', 'login', t => t.do(serverAction('authenticate', { method: 'login' }))],
|
|
307
|
-
['authenticated', 'login_success'],
|
|
308
|
-
)
|
|
309
|
-
.line(
|
|
310
|
-
'unauthenticated',
|
|
311
|
-
['authenticating', 'signup', t => t.do(serverAction('authenticate', { method: 'signup' }))],
|
|
312
|
-
['authenticated', 'signup_success'],
|
|
313
|
-
)
|
|
314
|
-
.line('authenticating', ['error', 'login_error'])
|
|
315
|
-
.line('authenticating', ['error', 'signup_error'])
|
|
316
|
-
.line('error', ['unauthenticated', 'retry'])
|
|
317
|
-
.line(
|
|
318
|
-
'authenticated',
|
|
319
|
-
['unauthenticated', 'logout', t => t.do(serverAction('destroy_session'))],
|
|
320
|
-
)
|
|
321
|
-
.build();
|
|
322
|
-
|
|
323
|
-
expect(result.states.unauthenticated.type).toBe('initial');
|
|
324
|
-
expect(result.transitions.login.actions![0].type).toBe('server:authenticate');
|
|
325
|
-
expect(result.transitions.signup.actions![0].type).toBe('server:authenticate');
|
|
326
|
-
expect(result.transitions.logout.actions![0].type).toBe('server:destroy_session');
|
|
327
|
-
expect(result.transitions.login_success.from).toBe('authenticating');
|
|
328
|
-
expect(result.transitions.signup_success.from).toBe('authenticating');
|
|
329
|
-
expect(result.transitions.retry.from).toBe('error');
|
|
330
|
-
expect(result.transitions.retry.to).toBe('unauthenticated');
|
|
331
|
-
|
|
332
|
-
// login_error and signup_error merge from-states
|
|
333
|
-
const loginErrorFrom = result.transitions.login_error.from;
|
|
334
|
-
expect(loginErrorFrom).toBe('authenticating');
|
|
335
|
-
});
|
|
336
|
-
});
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for BlueprintDependency composition types — routeConfig, slotMapping.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { describe, it, expect } from 'vitest';
|
|
6
|
-
import { defineBlueprint, type BlueprintDependency, type ModuleRouteConfig } from '../config/defineBlueprint';
|
|
7
|
-
|
|
8
|
-
describe('BlueprintDependency composition types', () => {
|
|
9
|
-
it('accepts routeConfig with prefix', () => {
|
|
10
|
-
const dep: BlueprintDependency = {
|
|
11
|
-
slug: 'mod-authentication',
|
|
12
|
-
version: '>=3.0.0',
|
|
13
|
-
routeConfig: {
|
|
14
|
-
prefix: '/auth',
|
|
15
|
-
},
|
|
16
|
-
};
|
|
17
|
-
expect(dep.routeConfig?.prefix).toBe('/auth');
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it('accepts routeConfig with per-route remapping', () => {
|
|
21
|
-
const dep: BlueprintDependency = {
|
|
22
|
-
slug: 'mod-authentication',
|
|
23
|
-
routeConfig: {
|
|
24
|
-
prefix: '/auth',
|
|
25
|
-
routes: {
|
|
26
|
-
'/login': '/',
|
|
27
|
-
'/signup': '/join',
|
|
28
|
-
'/profile': false,
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
|
-
};
|
|
32
|
-
expect(dep.routeConfig?.routes?.['/login']).toBe('/');
|
|
33
|
-
expect(dep.routeConfig?.routes?.['/profile']).toBe(false);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('accepts slotMapping', () => {
|
|
37
|
-
const dep: BlueprintDependency = {
|
|
38
|
-
slug: 'mod-authentication',
|
|
39
|
-
slotMapping: {
|
|
40
|
-
'auth:login-providers': 'app:social-login',
|
|
41
|
-
'auth:profile-sections': 'app:settings-tabs',
|
|
42
|
-
},
|
|
43
|
-
};
|
|
44
|
-
expect(dep.slotMapping?.['auth:login-providers']).toBe('app:social-login');
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('accepts config', () => {
|
|
48
|
-
const dep: BlueprintDependency = {
|
|
49
|
-
slug: 'mod-authentication',
|
|
50
|
-
config: {
|
|
51
|
-
layout: 'card',
|
|
52
|
-
methods: ['email-password', 'google'],
|
|
53
|
-
},
|
|
54
|
-
};
|
|
55
|
-
expect(dep.config?.layout).toBe('card');
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('works in defineBlueprint with full dependency config', () => {
|
|
59
|
-
const blueprint = defineBlueprint({
|
|
60
|
-
slug: 'employee-salary-tracking',
|
|
61
|
-
name: 'Employee Salary Tracking',
|
|
62
|
-
version: '1.0.0',
|
|
63
|
-
dependencies: [
|
|
64
|
-
{
|
|
65
|
-
slug: 'mod-authentication',
|
|
66
|
-
version: '>=3.0.0',
|
|
67
|
-
required: true,
|
|
68
|
-
routeConfig: {
|
|
69
|
-
prefix: '/auth',
|
|
70
|
-
routes: { '/login': '/' },
|
|
71
|
-
},
|
|
72
|
-
slotMapping: {
|
|
73
|
-
'auth:profile-sections': 'app:settings-tabs',
|
|
74
|
-
},
|
|
75
|
-
config: {
|
|
76
|
-
layout: 'split',
|
|
77
|
-
},
|
|
78
|
-
},
|
|
79
|
-
],
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
expect(blueprint.dependencies).toHaveLength(1);
|
|
83
|
-
expect(blueprint.dependencies![0].routeConfig?.prefix).toBe('/auth');
|
|
84
|
-
expect(blueprint.dependencies![0].slotMapping?.['auth:profile-sections']).toBe('app:settings-tabs');
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('ModuleRouteConfig type works independently', () => {
|
|
88
|
-
const config: ModuleRouteConfig = {
|
|
89
|
-
prefix: '/account',
|
|
90
|
-
routes: {
|
|
91
|
-
'/login': '/signin',
|
|
92
|
-
'/register': false,
|
|
93
|
-
},
|
|
94
|
-
};
|
|
95
|
-
expect(config.prefix).toBe('/account');
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('backward compatible — all new fields are optional', () => {
|
|
99
|
-
const dep: BlueprintDependency = {
|
|
100
|
-
slug: 'mod-auth',
|
|
101
|
-
};
|
|
102
|
-
expect(dep.routeConfig).toBeUndefined();
|
|
103
|
-
expect(dep.slotMapping).toBeUndefined();
|
|
104
|
-
expect(dep.config).toBeUndefined();
|
|
105
|
-
});
|
|
106
|
-
});
|
|
@@ -1,229 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { model, field, state } from '../builders';
|
|
3
|
-
import { approval, escalation, review, crud } from '../factories';
|
|
4
|
-
|
|
5
|
-
describe('approval factory', () => {
|
|
6
|
-
it('adds approve/reject transitions with defaults', () => {
|
|
7
|
-
const def = model('invoice')
|
|
8
|
-
.field('amount', field.currency())
|
|
9
|
-
.state('draft', state.initial())
|
|
10
|
-
.state('pending_approval')
|
|
11
|
-
.mixin(approval())
|
|
12
|
-
.build();
|
|
13
|
-
|
|
14
|
-
expect(def.transitions.approve).toBeDefined();
|
|
15
|
-
expect(def.transitions.approve.from).toBe('pending_approval');
|
|
16
|
-
expect(def.transitions.approve.to).toBe('approved');
|
|
17
|
-
expect(def.transitions.approve.roles).toContain('approver');
|
|
18
|
-
|
|
19
|
-
expect(def.transitions.reject).toBeDefined();
|
|
20
|
-
expect(def.transitions.reject.from).toBe('pending_approval');
|
|
21
|
-
expect(def.transitions.reject.to).toBe('rejected');
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('adds approval-related fields', () => {
|
|
25
|
-
const def = model('invoice')
|
|
26
|
-
.state('draft', state.initial())
|
|
27
|
-
.mixin(approval())
|
|
28
|
-
.build();
|
|
29
|
-
|
|
30
|
-
expect(def.fields.approvalStatus).toBeDefined();
|
|
31
|
-
expect(def.fields.approvedBy).toBeDefined();
|
|
32
|
-
expect(def.fields.approvedAt).toBeDefined();
|
|
33
|
-
expect(def.fields.rejectionReason).toBeDefined();
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('respects custom config', () => {
|
|
37
|
-
const def = model('invoice')
|
|
38
|
-
.state('draft', state.initial())
|
|
39
|
-
.state('review')
|
|
40
|
-
.mixin(approval({
|
|
41
|
-
approverRole: 'manager',
|
|
42
|
-
field: 'status',
|
|
43
|
-
fromState: 'review',
|
|
44
|
-
approvedState: 'done',
|
|
45
|
-
rejectedState: 'cancelled',
|
|
46
|
-
}))
|
|
47
|
-
.build();
|
|
48
|
-
|
|
49
|
-
expect(def.transitions.approve.from).toBe('review');
|
|
50
|
-
expect(def.transitions.approve.to).toBe('done');
|
|
51
|
-
expect(def.transitions.approve.roles).toContain('manager');
|
|
52
|
-
expect(def.transitions.reject.to).toBe('cancelled');
|
|
53
|
-
expect(def.fields.status).toBeDefined();
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('adds approver role', () => {
|
|
57
|
-
const def = model('invoice')
|
|
58
|
-
.state('draft', state.initial())
|
|
59
|
-
.mixin(approval())
|
|
60
|
-
.build();
|
|
61
|
-
|
|
62
|
-
expect(def.roles!.approver).toBeDefined();
|
|
63
|
-
expect(def.roles!.approver.permissions).toContain('approve');
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
describe('escalation factory', () => {
|
|
68
|
-
it('adds escalate transition with timeout', () => {
|
|
69
|
-
const def = model('ticket')
|
|
70
|
-
.state('pending_approval', state.initial())
|
|
71
|
-
.mixin(escalation({ timeout: '24h' }))
|
|
72
|
-
.build();
|
|
73
|
-
|
|
74
|
-
expect(def.transitions.escalate).toBeDefined();
|
|
75
|
-
expect(def.transitions.escalate.from).toBe('pending_approval');
|
|
76
|
-
expect(def.transitions.escalate.to).toBe('escalated');
|
|
77
|
-
expect(def.transitions.escalate.auto).toBe(true);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('sets timeout on the source state', () => {
|
|
81
|
-
const def = model('ticket')
|
|
82
|
-
.state('pending_approval', state.initial())
|
|
83
|
-
.mixin(escalation({ timeout: '2d', fromState: 'pending_approval' }))
|
|
84
|
-
.build();
|
|
85
|
-
|
|
86
|
-
expect(def.states.pending_approval.timeout).toBeDefined();
|
|
87
|
-
expect(def.states.pending_approval.timeout!.duration).toBe('2d');
|
|
88
|
-
expect(def.states.pending_approval.timeout!.fallback?.transition).toBe('escalate');
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('adds escalation fields', () => {
|
|
92
|
-
const def = model('ticket')
|
|
93
|
-
.state('open', state.initial())
|
|
94
|
-
.mixin(escalation({ timeout: '1h', fromState: 'open' }))
|
|
95
|
-
.build();
|
|
96
|
-
|
|
97
|
-
expect(def.fields.escalatedAt).toBeDefined();
|
|
98
|
-
expect(def.fields.escalationLevel).toBeDefined();
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
describe('review factory', () => {
|
|
103
|
-
it('adds submit/approve/revise cycle', () => {
|
|
104
|
-
const def = model('article')
|
|
105
|
-
.field('content', field.string())
|
|
106
|
-
.state('draft', state.initial())
|
|
107
|
-
.mixin(review())
|
|
108
|
-
.build();
|
|
109
|
-
|
|
110
|
-
expect(def.transitions.submit_for_review).toBeDefined();
|
|
111
|
-
expect(def.transitions.submit_for_review.from).toBe('draft');
|
|
112
|
-
expect(def.transitions.submit_for_review.to).toBe('in_review');
|
|
113
|
-
|
|
114
|
-
expect(def.transitions.approve_review).toBeDefined();
|
|
115
|
-
expect(def.transitions.approve_review.from).toBe('in_review');
|
|
116
|
-
expect(def.transitions.approve_review.to).toBe('approved');
|
|
117
|
-
|
|
118
|
-
expect(def.transitions.request_revisions).toBeDefined();
|
|
119
|
-
expect(def.transitions.request_revisions.from).toBe('in_review');
|
|
120
|
-
expect(def.transitions.request_revisions.to).toBe('revisions_requested');
|
|
121
|
-
|
|
122
|
-
expect(def.transitions.resubmit).toBeDefined();
|
|
123
|
-
expect(def.transitions.resubmit.from).toBe('revisions_requested');
|
|
124
|
-
expect(def.transitions.resubmit.to).toBe('in_review');
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it('adds review fields', () => {
|
|
128
|
-
const def = model('article')
|
|
129
|
-
.state('draft', state.initial())
|
|
130
|
-
.mixin(review())
|
|
131
|
-
.build();
|
|
132
|
-
|
|
133
|
-
expect(def.fields.reviewNotes).toBeDefined();
|
|
134
|
-
expect(def.fields.reviewedBy).toBeDefined();
|
|
135
|
-
expect(def.fields.reviewedAt).toBeDefined();
|
|
136
|
-
expect(def.fields.revisionCount).toBeDefined();
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('adds author and reviewer roles', () => {
|
|
140
|
-
const def = model('article')
|
|
141
|
-
.state('draft', state.initial())
|
|
142
|
-
.mixin(review())
|
|
143
|
-
.build();
|
|
144
|
-
|
|
145
|
-
expect(def.roles!.author).toBeDefined();
|
|
146
|
-
expect(def.roles!.reviewer).toBeDefined();
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it('respects custom role names', () => {
|
|
150
|
-
const def = model('article')
|
|
151
|
-
.state('draft', state.initial())
|
|
152
|
-
.mixin(review({ authorRole: 'writer', reviewerRole: 'editor' }))
|
|
153
|
-
.build();
|
|
154
|
-
|
|
155
|
-
expect(def.roles!.writer).toBeDefined();
|
|
156
|
-
expect(def.roles!.editor).toBeDefined();
|
|
157
|
-
expect(def.transitions.submit_for_review.roles).toContain('writer');
|
|
158
|
-
expect(def.transitions.approve_review.roles).toContain('editor');
|
|
159
|
-
});
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
describe('crud factory', () => {
|
|
163
|
-
it('adds draft/active/archived lifecycle (soft delete)', () => {
|
|
164
|
-
const def = model('product')
|
|
165
|
-
.field('name', field.string().required())
|
|
166
|
-
.mixin(crud())
|
|
167
|
-
.build();
|
|
168
|
-
|
|
169
|
-
expect(def.states.draft).toBeDefined();
|
|
170
|
-
expect(def.states.draft.type).toBe('initial');
|
|
171
|
-
expect(def.states.active).toBeDefined();
|
|
172
|
-
expect(def.states.archived).toBeDefined();
|
|
173
|
-
|
|
174
|
-
expect(def.transitions.publish).toBeDefined();
|
|
175
|
-
expect(def.transitions.update).toBeDefined();
|
|
176
|
-
expect(def.transitions.archive).toBeDefined();
|
|
177
|
-
expect(def.transitions.restore).toBeDefined();
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it('adds hard delete when softDelete=false', () => {
|
|
181
|
-
const def = model('temp')
|
|
182
|
-
.field('data', field.string())
|
|
183
|
-
.mixin(crud({ softDelete: false }))
|
|
184
|
-
.build();
|
|
185
|
-
|
|
186
|
-
expect(def.states.deleted).toBeDefined();
|
|
187
|
-
expect(def.states.deleted.type).toBe('end');
|
|
188
|
-
expect(def.transitions.delete_record).toBeDefined();
|
|
189
|
-
expect(def.transitions.archive).toBeUndefined();
|
|
190
|
-
expect(def.transitions.restore).toBeUndefined();
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
it('adds CRUD fields', () => {
|
|
194
|
-
const def = model('product')
|
|
195
|
-
.mixin(crud())
|
|
196
|
-
.build();
|
|
197
|
-
|
|
198
|
-
expect(def.fields.createdBy).toBeDefined();
|
|
199
|
-
expect(def.fields.createdAt).toBeDefined();
|
|
200
|
-
expect(def.fields.updatedAt).toBeDefined();
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
it('does not overwrite existing fields', () => {
|
|
204
|
-
const def = model('product')
|
|
205
|
-
.field('createdBy', field.string('system'))
|
|
206
|
-
.mixin(crud())
|
|
207
|
-
.build();
|
|
208
|
-
|
|
209
|
-
// Our manually set field should be preserved
|
|
210
|
-
expect(def.fields.createdBy.default).toBe('system');
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
it('composable: approval + escalation', () => {
|
|
214
|
-
const def = model('po')
|
|
215
|
-
.field('amount', field.currency())
|
|
216
|
-
.state('draft', state.initial())
|
|
217
|
-
.state('pending_approval')
|
|
218
|
-
.mixin(approval({ approverRole: 'manager' }))
|
|
219
|
-
.mixin(escalation({ timeout: '48h' }))
|
|
220
|
-
.build();
|
|
221
|
-
|
|
222
|
-
// Both patterns should coexist
|
|
223
|
-
expect(def.transitions.approve).toBeDefined();
|
|
224
|
-
expect(def.transitions.reject).toBeDefined();
|
|
225
|
-
expect(def.transitions.escalate).toBeDefined();
|
|
226
|
-
expect(def.states.escalated).toBeDefined();
|
|
227
|
-
expect(def.states.approved).toBeDefined();
|
|
228
|
-
});
|
|
229
|
-
});
|