@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.
Files changed (94) hide show
  1. package/README.md +112 -0
  2. package/dist/index.d.mts +27 -2
  3. package/dist/index.d.ts +27 -2
  4. package/dist/index.js +70 -3
  5. package/dist/index.mjs +74 -12
  6. package/package.json +4 -3
  7. package/package.json.backup +0 -41
  8. package/src/Blueprint.ts +0 -216
  9. package/src/__tests__/Blueprint.test.ts +0 -106
  10. package/src/__tests__/action-context.test.ts +0 -166
  11. package/src/__tests__/actionCreators.test.ts +0 -179
  12. package/src/__tests__/builders.test.ts +0 -336
  13. package/src/__tests__/defineBlueprint-composition.test.ts +0 -106
  14. package/src/__tests__/factories.test.ts +0 -229
  15. package/src/__tests__/loader.test.ts +0 -159
  16. package/src/__tests__/logger.test.ts +0 -70
  17. package/src/__tests__/type-inference.test.ts +0 -160
  18. package/src/__tests__/typed-transitions.test.ts +0 -126
  19. package/src/__tests__/useModuleConfig.test.ts +0 -61
  20. package/src/actionCreators.ts +0 -132
  21. package/src/actions.ts +0 -547
  22. package/src/atoms/index.ts +0 -600
  23. package/src/authoring.ts +0 -92
  24. package/src/browser-player.ts +0 -783
  25. package/src/builders.ts +0 -1342
  26. package/src/components/ExperienceWorkflowBridge.tsx +0 -123
  27. package/src/components/PlayerProvider.tsx +0 -43
  28. package/src/components/atoms/index.tsx +0 -269
  29. package/src/components/index.ts +0 -36
  30. package/src/conditions.ts +0 -692
  31. package/src/config/defineBlueprint.ts +0 -329
  32. package/src/config/defineModel.ts +0 -753
  33. package/src/config/defineWorkspace.ts +0 -24
  34. package/src/core/WorkflowRuntime.ts +0 -153
  35. package/src/factories.ts +0 -425
  36. package/src/grammar/index.ts +0 -173
  37. package/src/hooks/index.ts +0 -106
  38. package/src/hooks/useAuth.ts +0 -288
  39. package/src/hooks/useChannel.ts +0 -304
  40. package/src/hooks/useComputed.ts +0 -154
  41. package/src/hooks/useDomainSubscription.ts +0 -110
  42. package/src/hooks/useDuringAction.ts +0 -99
  43. package/src/hooks/useExperienceState.ts +0 -59
  44. package/src/hooks/useExpressionLibrary.ts +0 -129
  45. package/src/hooks/useForm.ts +0 -352
  46. package/src/hooks/useGeolocation.ts +0 -207
  47. package/src/hooks/useMapView.ts +0 -259
  48. package/src/hooks/useMiddleware.ts +0 -291
  49. package/src/hooks/useModel.ts +0 -363
  50. package/src/hooks/useModule.ts +0 -59
  51. package/src/hooks/useModuleConfig.ts +0 -61
  52. package/src/hooks/useMutation.ts +0 -237
  53. package/src/hooks/useNotification.ts +0 -151
  54. package/src/hooks/useOnChange.ts +0 -30
  55. package/src/hooks/useOnEnter.ts +0 -59
  56. package/src/hooks/useOnEvent.ts +0 -37
  57. package/src/hooks/useOnExit.ts +0 -27
  58. package/src/hooks/useOnTransition.ts +0 -30
  59. package/src/hooks/usePackage.ts +0 -128
  60. package/src/hooks/useParams.ts +0 -33
  61. package/src/hooks/usePlayer.ts +0 -308
  62. package/src/hooks/useQuery.ts +0 -184
  63. package/src/hooks/useRealtimeQuery.ts +0 -222
  64. package/src/hooks/useRole.ts +0 -191
  65. package/src/hooks/useRouteParams.ts +0 -100
  66. package/src/hooks/useRouter.ts +0 -347
  67. package/src/hooks/useServerAction.ts +0 -178
  68. package/src/hooks/useServerState.ts +0 -284
  69. package/src/hooks/useToast.ts +0 -164
  70. package/src/hooks/useTransition.ts +0 -39
  71. package/src/hooks/useView.ts +0 -102
  72. package/src/hooks/useWhileIn.ts +0 -48
  73. package/src/hooks/useWorkflow.ts +0 -63
  74. package/src/index.ts +0 -465
  75. package/src/loader/experience-workflow-loader.ts +0 -192
  76. package/src/loader/index.ts +0 -6
  77. package/src/local/LocalEngine.ts +0 -388
  78. package/src/local/LocalEngineAdapter.ts +0 -175
  79. package/src/local/LocalEngineContext.ts +0 -30
  80. package/src/logger.ts +0 -37
  81. package/src/mixins.ts +0 -1160
  82. package/src/providers/RuntimeContext.ts +0 -20
  83. package/src/providers/WorkflowProvider.tsx +0 -28
  84. package/src/routing/instance-key.ts +0 -107
  85. package/src/server/transition-context.ts +0 -172
  86. package/src/testing/index.ts +0 -9
  87. package/src/testing/useBlueprintTestRunner.ts +0 -91
  88. package/src/testing/useGraphAnalysis.ts +0 -18
  89. package/src/testing/useTestRunner.ts +0 -77
  90. package/src/testing.ts +0 -995
  91. package/src/types/workflow-inference.ts +0 -158
  92. package/src/types.ts +0 -114
  93. package/tsconfig.json +0 -27
  94. package/vitest.config.ts +0 -8
@@ -1,159 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { validateExperienceWorkflow, normalizeDefinition } from '../loader';
3
-
4
- const validDef = {
5
- id: 'test-1',
6
- slug: 'test-workflow',
7
- states: [
8
- { name: 'start', type: 'START' },
9
- { name: 'active', type: 'REGULAR' },
10
- { name: 'done', type: 'END' },
11
- ],
12
- transitions: [
13
- { name: 'begin', from: ['start'], to: 'active' },
14
- { name: 'finish', from: ['active'], to: 'done' },
15
- ],
16
- };
17
-
18
- describe('validateExperienceWorkflow', () => {
19
- it('validates a correct definition', () => {
20
- const result = validateExperienceWorkflow(validDef);
21
- expect(result.valid).toBe(true);
22
- expect(result.errors).toHaveLength(0);
23
- });
24
-
25
- it('rejects non-object input', () => {
26
- expect(validateExperienceWorkflow(null).valid).toBe(false);
27
- expect(validateExperienceWorkflow('string').valid).toBe(false);
28
- expect(validateExperienceWorkflow(42).valid).toBe(false);
29
- });
30
-
31
- it('requires id and slug', () => {
32
- const result = validateExperienceWorkflow({ states: [{ name: 'x', type: 'START' }], transitions: [] });
33
- expect(result.valid).toBe(false);
34
- expect(result.errors.some(e => e.includes('id'))).toBe(true);
35
- expect(result.errors.some(e => e.includes('slug'))).toBe(true);
36
- });
37
-
38
- it('requires at least one state', () => {
39
- const result = validateExperienceWorkflow({ id: '1', slug: 's', states: [], transitions: [] });
40
- expect(result.valid).toBe(false);
41
- expect(result.errors.some(e => e.includes('at least one state'))).toBe(true);
42
- });
43
-
44
- it('requires a START state', () => {
45
- const result = validateExperienceWorkflow({
46
- id: '1', slug: 's',
47
- states: [{ name: 'x', type: 'REGULAR' }],
48
- transitions: [],
49
- });
50
- expect(result.valid).toBe(false);
51
- expect(result.errors.some(e => e.includes('START'))).toBe(true);
52
- });
53
-
54
- it('detects duplicate state names', () => {
55
- const result = validateExperienceWorkflow({
56
- id: '1', slug: 's',
57
- states: [{ name: 'x', type: 'START' }, { name: 'x', type: 'REGULAR' }],
58
- transitions: [],
59
- });
60
- expect(result.valid).toBe(false);
61
- expect(result.errors.some(e => e.includes('Duplicate'))).toBe(true);
62
- });
63
-
64
- it('detects unknown transition target states', () => {
65
- const result = validateExperienceWorkflow({
66
- id: '1', slug: 's',
67
- states: [{ name: 'start', type: 'START' }],
68
- transitions: [{ name: 't', from: ['start'], to: 'nonexistent' }],
69
- });
70
- expect(result.valid).toBe(false);
71
- expect(result.errors.some(e => e.includes('unknown state'))).toBe(true);
72
- });
73
-
74
- it('detects unknown transition from states', () => {
75
- const result = validateExperienceWorkflow({
76
- id: '1', slug: 's',
77
- states: [{ name: 'start', type: 'START' }, { name: 'end', type: 'END' }],
78
- transitions: [{ name: 't', from: ['ghost'], to: 'end' }],
79
- });
80
- expect(result.valid).toBe(false);
81
- expect(result.errors.some(e => e.includes('unknown "from" state'))).toBe(true);
82
- });
83
-
84
- it('validates on_event subscriptions', () => {
85
- const result = validateExperienceWorkflow({
86
- id: '1', slug: 's',
87
- states: [{
88
- name: 'start', type: 'START',
89
- on_event: [{ actions: [{ type: 'log', config: {} }] }], // missing match
90
- }],
91
- transitions: [],
92
- });
93
- expect(result.valid).toBe(false);
94
- expect(result.errors.some(e => e.includes('match'))).toBe(true);
95
- });
96
-
97
- it('warns on on_event with empty actions', () => {
98
- const result = validateExperienceWorkflow({
99
- id: '1', slug: 's',
100
- states: [{
101
- name: 'start', type: 'START',
102
- on_event: [{ match: 'test.*', actions: [] }],
103
- }],
104
- transitions: [],
105
- });
106
- expect(result.valid).toBe(true); // warning, not error
107
- expect(result.warnings.some(w => w.includes('no actions'))).toBe(true);
108
- });
109
- });
110
-
111
- describe('normalizeDefinition', () => {
112
- it('normalizes a valid definition', () => {
113
- const def = normalizeDefinition(validDef);
114
- expect(def.id).toBe('test-1');
115
- expect(def.slug).toBe('test-workflow');
116
- expect(def.states).toHaveLength(3);
117
- expect(def.transitions).toHaveLength(2);
118
- });
119
-
120
- it('defaults state type to REGULAR', () => {
121
- const def = normalizeDefinition({
122
- id: '1', slug: 's',
123
- states: [{ name: 'x' }],
124
- transitions: [],
125
- });
126
- expect(def.states[0].type).toBe('REGULAR');
127
- });
128
-
129
- it('coerces string "from" to array', () => {
130
- const def = normalizeDefinition({
131
- id: '1', slug: 's',
132
- states: [{ name: 'a', type: 'START' }, { name: 'b', type: 'END' }],
133
- transitions: [{ name: 't', from: 'a', to: 'b' }],
134
- });
135
- expect(def.transitions[0].from).toEqual(['a']);
136
- });
137
-
138
- it('normalizes on_event with "pattern" field to "match"', () => {
139
- const def = normalizeDefinition({
140
- id: '1', slug: 's',
141
- states: [{
142
- name: 'x', type: 'START',
143
- on_event: [{ pattern: 'test.*', actions: [{ type: 'log', config: {} }] }],
144
- }],
145
- transitions: [],
146
- });
147
- expect(def.states[0].on_event?.[0].match).toBe('test.*');
148
- });
149
-
150
- it('preserves state_views and experience metadata', () => {
151
- const def = normalizeDefinition({
152
- ...validDef,
153
- state_views: { active: { title: 'Active View' } },
154
- experience: { default_layout: 'grid' },
155
- });
156
- expect(def.state_views?.active.title).toBe('Active View');
157
- expect(def.experience?.default_layout).toBe('grid');
158
- });
159
- });
@@ -1,70 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { setPlayerDebug, isPlayerDebug, playerLog } from '../logger';
3
-
4
- describe('Player Logger', () => {
5
- beforeEach(() => {
6
- setPlayerDebug(false);
7
- });
8
-
9
- it('toggles debug mode', () => {
10
- expect(isPlayerDebug()).toBe(false);
11
- setPlayerDebug(true);
12
- expect(isPlayerDebug()).toBe(true);
13
- });
14
-
15
- it('suppresses debug logs when debug is off', () => {
16
- const spy = vi.spyOn(console, 'debug').mockImplementation(() => {});
17
- playerLog({ level: 'debug', category: 'event_match', message: 'test' });
18
- expect(spy).not.toHaveBeenCalled();
19
- spy.mockRestore();
20
- });
21
-
22
- it('emits debug logs when debug is on', () => {
23
- setPlayerDebug(true);
24
- const spy = vi.spyOn(console, 'debug').mockImplementation(() => {});
25
- playerLog({ level: 'debug', category: 'event_match', message: 'test' });
26
- expect(spy).toHaveBeenCalledOnce();
27
- spy.mockRestore();
28
- });
29
-
30
- it('emits warn logs regardless of debug setting', () => {
31
- const spy = vi.spyOn(console, 'warn').mockImplementation(() => {});
32
- playerLog({ level: 'warn', category: 'action_dispatch', message: 'warning' });
33
- expect(spy).toHaveBeenCalledOnce();
34
- spy.mockRestore();
35
- });
36
-
37
- it('emits error logs', () => {
38
- const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
39
- playerLog({ level: 'error', category: 'transition', message: 'error msg' });
40
- expect(spy).toHaveBeenCalledOnce();
41
- spy.mockRestore();
42
- });
43
-
44
- it('emits info logs', () => {
45
- const spy = vi.spyOn(console, 'info').mockImplementation(() => {});
46
- playerLog({ level: 'info', category: 'lifecycle', message: 'info msg' });
47
- expect(spy).toHaveBeenCalledOnce();
48
- spy.mockRestore();
49
- });
50
-
51
- it('includes category prefix in log message', () => {
52
- const spy = vi.spyOn(console, 'warn').mockImplementation(() => {});
53
- playerLog({ level: 'warn', category: 'event_match', message: 'matched' });
54
- expect(spy).toHaveBeenCalledWith(
55
- expect.stringContaining('[player-web:event_match]'),
56
- expect.anything(),
57
- );
58
- spy.mockRestore();
59
- });
60
-
61
- it('passes data object to console', () => {
62
- const spy = vi.spyOn(console, 'info').mockImplementation(() => {});
63
- playerLog({ level: 'info', category: 'lifecycle', message: 'test', data: { key: 'val' } });
64
- expect(spy).toHaveBeenCalledWith(
65
- expect.any(String),
66
- { key: 'val' },
67
- );
68
- spy.mockRestore();
69
- });
70
- });
@@ -1,160 +0,0 @@
1
- import { describe, it, expect, expectTypeOf } from 'vitest';
2
- import { defineModel } from '../config/defineModel';
3
- import type {
4
- InferFields,
5
- InferTransitions,
6
- InferStates,
7
- InferSlug,
8
- InferFieldNames,
9
- } from '../types/workflow-inference';
10
-
11
- // =============================================================================
12
- // Test model definition
13
- // =============================================================================
14
-
15
- const employeeModel = defineModel({
16
- slug: 'employee',
17
- version: '1.0.0',
18
- category: 'data',
19
- fields: {
20
- name: { type: 'string', required: true },
21
- email: { type: 'email', required: true },
22
- salary: { type: 'currency', default: 0 },
23
- isActive: { type: 'boolean', default: true },
24
- startDate: { type: 'date' },
25
- tags: { type: 'array' },
26
- metadata: { type: 'object' },
27
- department: { type: 'string', enum: ['engineering', 'sales', 'hr'] as const },
28
- rating: { type: 'rating' },
29
- },
30
- states: {
31
- onboarding: { type: 'initial' },
32
- active: {},
33
- on_leave: {},
34
- terminated: { type: 'end' },
35
- },
36
- transitions: {
37
- activate: { from: 'onboarding', to: 'active' },
38
- request_leave: { from: 'active', to: 'on_leave' },
39
- return_from_leave: { from: 'on_leave', to: 'active' },
40
- terminate: { from: ['active', 'on_leave'], to: 'terminated' },
41
- },
42
- } as const);
43
-
44
- type EmployeeFields = InferFields<typeof employeeModel>;
45
- type EmployeeTransitions = InferTransitions<typeof employeeModel>;
46
- type EmployeeStates = InferStates<typeof employeeModel>;
47
-
48
- // =============================================================================
49
- // Type-level tests
50
- // =============================================================================
51
-
52
- describe('InferFields', () => {
53
- it('maps string-family types to string', () => {
54
- expectTypeOf<EmployeeFields['name']>().toEqualTypeOf<string>();
55
- expectTypeOf<EmployeeFields['email']>().toEqualTypeOf<string>();
56
- expectTypeOf<EmployeeFields['department']>().toEqualTypeOf<string>();
57
- expectTypeOf<EmployeeFields['startDate']>().toEqualTypeOf<string>();
58
- });
59
-
60
- it('maps numeric types to number', () => {
61
- expectTypeOf<EmployeeFields['salary']>().toEqualTypeOf<number>();
62
- expectTypeOf<EmployeeFields['rating']>().toEqualTypeOf<number>();
63
- });
64
-
65
- it('maps boolean type to boolean', () => {
66
- expectTypeOf<EmployeeFields['isActive']>().toEqualTypeOf<boolean>();
67
- });
68
-
69
- it('maps complex types correctly', () => {
70
- expectTypeOf<EmployeeFields['tags']>().toEqualTypeOf<unknown[]>();
71
- expectTypeOf<EmployeeFields['metadata']>().toEqualTypeOf<Record<string, unknown>>();
72
- });
73
-
74
- it('produces an object with all field keys', () => {
75
- expectTypeOf<EmployeeFields>().toHaveProperty('name');
76
- expectTypeOf<EmployeeFields>().toHaveProperty('email');
77
- expectTypeOf<EmployeeFields>().toHaveProperty('salary');
78
- expectTypeOf<EmployeeFields>().toHaveProperty('isActive');
79
- expectTypeOf<EmployeeFields>().toHaveProperty('startDate');
80
- expectTypeOf<EmployeeFields>().toHaveProperty('tags');
81
- expectTypeOf<EmployeeFields>().toHaveProperty('metadata');
82
- expectTypeOf<EmployeeFields>().toHaveProperty('department');
83
- expectTypeOf<EmployeeFields>().toHaveProperty('rating');
84
- });
85
-
86
- it('falls back to Record<string, unknown> for non-model types', () => {
87
- type NoFields = InferFields<{ slug: 'x' }>;
88
- expectTypeOf<NoFields>().toEqualTypeOf<Record<string, unknown>>();
89
- });
90
- });
91
-
92
- describe('InferTransitions', () => {
93
- it('extracts transition names as a union', () => {
94
- expectTypeOf<EmployeeTransitions>().toEqualTypeOf<
95
- 'activate' | 'request_leave' | 'return_from_leave' | 'terminate'
96
- >();
97
- });
98
-
99
- it('falls back to string for non-model types', () => {
100
- type NoTrans = InferTransitions<{ slug: 'x' }>;
101
- expectTypeOf<NoTrans>().toEqualTypeOf<string>();
102
- });
103
- });
104
-
105
- describe('InferStates', () => {
106
- it('extracts state names as a union', () => {
107
- expectTypeOf<EmployeeStates>().toEqualTypeOf<
108
- 'onboarding' | 'active' | 'on_leave' | 'terminated'
109
- >();
110
- });
111
-
112
- it('falls back to string for non-model types', () => {
113
- type NoStates = InferStates<{ slug: 'x' }>;
114
- expectTypeOf<NoStates>().toEqualTypeOf<string>();
115
- });
116
- });
117
-
118
- describe('InferSlug', () => {
119
- it('extracts the literal slug type', () => {
120
- expectTypeOf<InferSlug<typeof employeeModel>>().toEqualTypeOf<'employee'>();
121
- });
122
- });
123
-
124
- describe('InferFieldNames', () => {
125
- it('extracts field name union', () => {
126
- expectTypeOf<InferFieldNames<typeof employeeModel>>().toEqualTypeOf<
127
- 'name' | 'email' | 'salary' | 'isActive' | 'startDate' | 'tags' | 'metadata' | 'department' | 'rating'
128
- >();
129
- });
130
- });
131
-
132
- describe('defineModel', () => {
133
- it('returns the definition unchanged at runtime', () => {
134
- expect(employeeModel.slug).toBe('employee');
135
- expect(Object.keys(employeeModel.fields)).toHaveLength(9);
136
- expect(Object.keys(employeeModel.states)).toHaveLength(4);
137
- expect(Object.keys(employeeModel.transitions)).toHaveLength(4);
138
- });
139
-
140
- it('preserves const literal types', () => {
141
- // The slug should be the literal 'employee', not string
142
- expectTypeOf(employeeModel.slug).toEqualTypeOf<'employee'>();
143
- // Field keys should be literal union
144
- expectTypeOf<keyof typeof employeeModel.fields>().toEqualTypeOf<
145
- 'name' | 'email' | 'salary' | 'isActive' | 'startDate' | 'tags' | 'metadata' | 'department' | 'rating'
146
- >();
147
- });
148
-
149
- it('throws in dev mode when slug is missing', () => {
150
- expect(() => defineModel({ slug: '', fields: {}, states: {}, transitions: {} })).toThrow(
151
- 'slug is required',
152
- );
153
- });
154
-
155
- it('throws in dev mode when fields is missing', () => {
156
- expect(() => defineModel({ slug: 'x', fields: null as any, states: {}, transitions: {} })).toThrow(
157
- 'fields object is required',
158
- );
159
- });
160
- });
@@ -1,126 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { model, field, state, transition, TypedTransitionBuilder } from '../builders';
3
- import { setField, logEvent } from '../actions';
4
-
5
- describe('TypedTransitionBuilder — type-safe state references', () => {
6
- it('accepts a callback that builds a typed transition', () => {
7
- const result = model('invoice')
8
- .state('draft', state.initial())
9
- .state('sent')
10
- .state('paid', state.end())
11
- .transition('send', t => t.from('draft').to('sent'))
12
- .build();
13
-
14
- expect(result.transitions.send.from).toBe('draft');
15
- expect(result.transitions.send.to).toBe('sent');
16
- });
17
-
18
- it('supports require, roles, and when in callback form', () => {
19
- const result = model('invoice')
20
- .state('draft', state.initial())
21
- .state('review')
22
- .state('approved', state.end())
23
- .transition('submit', t =>
24
- t.from('draft').to('review').require('amount', 'title')
25
- )
26
- .transition('approve', t =>
27
- t.from('review').to('approved').roles('manager').when('state_data.amount < 10000')
28
- )
29
- .build();
30
-
31
- expect(result.transitions.submit.requiredFields).toEqual(['amount', 'title']);
32
- expect(result.transitions.approve.roles).toEqual(['manager']);
33
- expect(result.transitions.approve.conditions).toHaveLength(1);
34
- });
35
-
36
- it('supports auto transitions in callback form', () => {
37
- const result = model('pipeline')
38
- .state('processing', state.initial())
39
- .state('done', state.end())
40
- .transition('complete', t =>
41
- t.from('processing').to('done').auto()
42
- )
43
- .build();
44
-
45
- expect(result.transitions.complete.auto).toBe(true);
46
- });
47
-
48
- it('supports multiple from states in callback form', () => {
49
- const result = model('order')
50
- .state('placed', state.initial())
51
- .state('confirmed')
52
- .state('cancelled', state.end())
53
- .transition('cancel', t => t.from('placed', 'confirmed').to('cancelled'))
54
- .build();
55
-
56
- expect(result.transitions.cancel.from).toEqual(['placed', 'confirmed']);
57
- expect(result.transitions.cancel.to).toBe('cancelled');
58
- });
59
-
60
- it('coexists with untyped transition() calls', () => {
61
- const result = model('test')
62
- .state('a', state.initial())
63
- .state('b')
64
- .state('c', state.end())
65
- // typed callback
66
- .transition('ab', t => t.from('a').to('b'))
67
- // untyped existing API
68
- .transition('bc', transition.from('b').to('c'))
69
- .build();
70
-
71
- expect(result.transitions.ab).toEqual({ from: 'a', to: 'b' });
72
- expect(result.transitions.bc).toEqual({ from: 'b', to: 'c' });
73
- });
74
-
75
- it('coexists with plain object descriptors', () => {
76
- const result = model('test')
77
- .state('x', state.initial())
78
- .state('y', state.end())
79
- .transition('go', { from: 'x', to: 'y' })
80
- .build();
81
-
82
- expect(result.transitions.go.from).toBe('x');
83
- expect(result.transitions.go.to).toBe('y');
84
- });
85
-
86
- it('supports description in callback form', () => {
87
- const result = model('test')
88
- .state('a', state.initial())
89
- .state('b', state.end())
90
- .transition('go', t => t.from('a').to('b').description('Move from a to b'))
91
- .build();
92
-
93
- expect(result.transitions.go.description).toBe('Move from a to b');
94
- });
95
-
96
- it('supports do() with actions in callback form', () => {
97
- const result = model('test')
98
- .state('a', state.initial())
99
- .state('b', state.end())
100
- .transition('go', t =>
101
- t.from('a').to('b').do(setField('status', '"done"'), logEvent('completed'))
102
- )
103
- .build();
104
-
105
- expect(result.transitions.go.actions).toHaveLength(2);
106
- expect(result.transitions.go.actions![0].type).toBe('set_field');
107
- expect(result.transitions.go.actions![1].type).toBe('log_event');
108
- });
109
- });
110
-
111
- describe('TypedTransitionBuilder class standalone', () => {
112
- it('builds a valid TransitionDescriptor', () => {
113
- const builder = new TypedTransitionBuilder<'a' | 'b'>();
114
- builder.from('a').to('b').require('name');
115
- const desc = builder.build();
116
-
117
- expect(desc.from).toBe('a');
118
- expect(desc.to).toBe('b');
119
- expect(desc.requiredFields).toEqual(['name']);
120
- });
121
-
122
- it('throws when from/to are missing', () => {
123
- const builder = new TypedTransitionBuilder<'a'>();
124
- expect(() => builder.build()).toThrow('from() is required');
125
- });
126
- });
@@ -1,61 +0,0 @@
1
- /**
2
- * Tests for useModuleConfig — config merging and module lookup.
3
- */
4
-
5
- import { describe, it, expect, beforeEach } from 'vitest';
6
- import {
7
- setInstalledModules,
8
- setModuleConfigDefaults,
9
- getInstalledModule,
10
- getInstalledModules,
11
- type InstalledModuleRef,
12
- } from '../hooks/useModuleConfig';
13
-
14
- describe('useModuleConfig store', () => {
15
- beforeEach(() => {
16
- setInstalledModules([]);
17
- });
18
-
19
- it('stores and retrieves installed modules', () => {
20
- const modules: InstalledModuleRef[] = [
21
- { slug: 'mod-auth', moduleId: 'auth-001', config: { layout: 'card' } },
22
- { slug: 'mod-notif', moduleId: 'notif-001' },
23
- ];
24
-
25
- setInstalledModules(modules);
26
-
27
- expect(getInstalledModules()).toHaveLength(2);
28
- expect(getInstalledModule('mod-auth')?.config?.layout).toBe('card');
29
- expect(getInstalledModule('mod-notif')).toBeDefined();
30
- expect(getInstalledModule('mod-unknown')).toBeUndefined();
31
- });
32
-
33
- it('stores routeConfig and slotMapping on installed module', () => {
34
- setInstalledModules([{
35
- slug: 'mod-auth',
36
- moduleId: 'auth-001',
37
- routeConfig: { prefix: '/auth', routes: { '/login': '/' } },
38
- slotMapping: { 'auth:profile': 'app:settings' },
39
- }]);
40
-
41
- const mod = getInstalledModule('mod-auth');
42
- expect(mod?.routeConfig?.prefix).toBe('/auth');
43
- expect(mod?.routeConfig?.routes?.['/login']).toBe('/');
44
- expect(mod?.slotMapping?.['auth:profile']).toBe('app:settings');
45
- });
46
-
47
- it('setModuleConfigDefaults works', () => {
48
- setModuleConfigDefaults('mod-auth', { layout: 'split', methods: ['email'] });
49
-
50
- // Config defaults are stored and can be merged with install-time config
51
- // (useModuleConfig hook merges them, but we test the store directly here)
52
- setInstalledModules([{
53
- slug: 'mod-auth',
54
- moduleId: 'auth-001',
55
- config: { layout: 'card' },
56
- }]);
57
-
58
- const mod = getInstalledModule('mod-auth');
59
- expect(mod?.config?.layout).toBe('card'); // install-time wins
60
- });
61
- });