@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,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
|
-
});
|