@mmapp/react-compiler 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/ATOM-PIPELINE.md +144 -0
- package/README.md +88 -40
- package/dist/babel/index.js +113 -6
- package/dist/babel/index.mjs +2 -2
- package/dist/chunk-3USIFFE4.mjs +2190 -0
- package/dist/chunk-45YMGEVT.mjs +186 -0
- package/dist/chunk-4FN2AISW.mjs +148 -0
- package/dist/chunk-4OPI5L7G.mjs +2593 -0
- package/dist/chunk-4RYTKOOJ.mjs +186 -0
- package/dist/chunk-5RKTOVR5.mjs +244 -0
- package/dist/chunk-5YDMOO4X.mjs +214 -0
- package/dist/chunk-64ZWEMLJ.mjs +148 -0
- package/dist/chunk-6XP4KSWQ.mjs +2190 -0
- package/dist/chunk-72QWL54I.mjs +175 -0
- package/dist/chunk-7B4TRI7C.mjs +4835 -0
- package/dist/chunk-7ZKGHTNB.mjs +4952 -0
- package/dist/chunk-CIESM3BP.mjs +33 -0
- package/dist/chunk-DE3ZGQAC.mjs +148 -0
- package/dist/chunk-DMCY3BBG.mjs +1933 -0
- package/dist/chunk-DPIK3PJS.mjs +244 -0
- package/dist/chunk-E5IVH4RE.mjs +186 -0
- package/dist/chunk-E6FZNUR5.mjs +4953 -0
- package/dist/chunk-EJRBDQDP.mjs +2607 -0
- package/dist/chunk-ELO4TXJL.mjs +186 -0
- package/dist/chunk-FKRO52XH.mjs +3446 -0
- package/dist/chunk-FL4YAKU6.mjs +4941 -0
- package/dist/chunk-FYT47UBU.mjs +5076 -0
- package/dist/chunk-GCLGPOJZ.mjs +148 -0
- package/dist/chunk-GXB4JOP7.mjs +5072 -0
- package/dist/chunk-HFXOUMTD.mjs +175 -0
- package/dist/chunk-HWIZ47US.mjs +214 -0
- package/dist/chunk-IB7MNPQL.mjs +4953 -0
- package/dist/chunk-ICSIHQCG.mjs +148 -0
- package/dist/chunk-JLA5VNQ3.mjs +186 -0
- package/dist/chunk-JQLWFCTM.mjs +214 -0
- package/dist/chunk-KFJJCQAL.mjs +148 -0
- package/dist/chunk-KJUIIEQE.mjs +186 -0
- package/dist/chunk-KNWTHRVQ.mjs +175 -0
- package/dist/chunk-KSG4XSZF.mjs +175 -0
- package/dist/chunk-LF5N6DOU.mjs +175 -0
- package/dist/chunk-LJQCM2IM.mjs +214 -0
- package/dist/chunk-NW6555WJ.mjs +186 -0
- package/dist/chunk-OMZE6VLQ.mjs +214 -0
- package/dist/chunk-P4BR7WVO.mjs +2190 -0
- package/dist/chunk-QQHVYH2X.mjs +244 -0
- package/dist/chunk-S5QLWLLT.mjs +186 -0
- package/dist/chunk-SCWGT2FY.mjs +2190 -0
- package/dist/chunk-SMKJUSB3.mjs +2190 -0
- package/dist/chunk-VCAY2KGM.mjs +175 -0
- package/dist/chunk-WECAV6QB.mjs +148 -0
- package/dist/chunk-WMKBXUCE.mjs +3228 -0
- package/dist/chunk-XAJ5BKKL.mjs +4947 -0
- package/dist/chunk-XG2X7AEA.mjs +175 -0
- package/dist/chunk-XG7Z23NQ.mjs +148 -0
- package/dist/chunk-XWZAOCQ7.mjs +2607 -0
- package/dist/chunk-Y6MA7ULW.mjs +148 -0
- package/dist/chunk-YMS7Q7LG.mjs +214 -0
- package/dist/chunk-ZA37XTGA.mjs +175 -0
- package/dist/cli/index.js +1616 -366
- package/dist/cli/index.mjs +8 -8
- package/dist/codemod/cli.mjs +1 -1
- package/dist/codemod/index.mjs +1 -1
- package/dist/dev-server-RmGHIntF.d.mts +113 -0
- package/dist/dev-server-RmGHIntF.d.ts +113 -0
- package/dist/dev-server.d.mts +1 -1
- package/dist/dev-server.d.ts +1 -1
- package/dist/dev-server.js +982 -53
- package/dist/dev-server.mjs +5 -5
- package/dist/envelope.js +113 -6
- package/dist/envelope.mjs +3 -3
- package/dist/index.d.mts +5 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +992 -63
- package/dist/index.mjs +8 -8
- package/{src/cli/init.ts → dist/init-7JQMAAXS.mjs} +70 -95
- package/dist/init-EHO4VQ22.mjs +369 -0
- package/dist/init-UC3FWPIW.mjs +367 -0
- package/dist/init-UNSMVKIK.mjs +366 -0
- package/dist/init-UNV5XIDE.mjs +367 -0
- package/dist/project-compiler-2P4N4DR7.mjs +10 -0
- package/dist/project-compiler-D2LCC27O.mjs +10 -0
- package/dist/project-compiler-EJ3GANJE.mjs +10 -0
- package/dist/project-compiler-LOQKVRZJ.mjs +10 -0
- package/dist/project-compiler-RQ6OQKRM.mjs +10 -0
- package/dist/project-compiler-VWNNCHGO.mjs +10 -0
- package/dist/project-compiler-XVAAU4C5.mjs +10 -0
- package/dist/project-compiler-YES5FGMD.mjs +10 -0
- package/dist/project-compiler-ZKMQDLGU.mjs +10 -0
- package/dist/project-decompiler-FLXCEJHS.mjs +7 -0
- package/dist/project-decompiler-VLPR22QF.mjs +7 -0
- package/dist/pull-FUS5QYZS.mjs +109 -0
- package/dist/pull-LD5ENLGY.mjs +109 -0
- package/dist/testing/index.js +113 -6
- package/dist/testing/index.mjs +2 -2
- package/dist/vite/index.js +113 -6
- package/dist/vite/index.mjs +3 -3
- package/examples/uber-app/app/admin/fleet.tsx +19 -19
- package/package.json +4 -3
- package/compile-blueprint-chat.mjs +0 -99
- package/compile-blueprint-glass-console.mjs +0 -98
- package/compile-chat-defs.mjs +0 -92
- package/examples/uber-app/tests/payment.test.tsx +0 -129
- package/examples/uber-app/tests/ride-flow.test.tsx +0 -123
- package/package.json.backup +0 -86
- package/scripts/decompile.ts +0 -226
- package/scripts/seed-auth.ts +0 -267
- package/scripts/seed-uber.ts +0 -248
- package/scripts/validate-uber.ts +0 -119
- package/seed-blueprint-chat.mjs +0 -444
- package/seed-blueprint-glass-console.mjs +0 -445
- package/seed-compiled.mjs +0 -318
- package/src/RoundTripValidator.ts +0 -400
- package/src/__tests__/atom-rendering-coverage.test.ts +0 -680
- package/src/__tests__/auth-module-compilation.test.ts +0 -247
- package/src/__tests__/auth-template-compilation.test.ts +0 -589
- package/src/__tests__/change-extractor.test.ts +0 -142
- package/src/__tests__/cli-pull.test.ts +0 -73
- package/src/__tests__/cli-test.test.ts +0 -72
- package/src/__tests__/component-extractor.test.ts +0 -331
- package/src/__tests__/context-extractor.test.ts +0 -145
- package/src/__tests__/decompiler.test.ts +0 -718
- package/src/__tests__/define-blueprint.test.ts +0 -133
- package/src/__tests__/definition-validator.test.ts +0 -519
- package/src/__tests__/during-extractor.test.ts +0 -152
- package/src/__tests__/effect-extractor.test.ts +0 -107
- package/src/__tests__/event-emission.test.ts +0 -127
- package/src/__tests__/examples.test.ts +0 -236
- package/src/__tests__/full-blueprint-coverage.test.ts +0 -1221
- package/src/__tests__/golden-suite.test.ts +0 -403
- package/src/__tests__/grammar-island-extractor.test.ts +0 -289
- package/src/__tests__/instance-key.test.ts +0 -82
- package/src/__tests__/ir-migration.test.ts +0 -255
- package/src/__tests__/lock-file.test.ts +0 -117
- package/src/__tests__/model-extractor.test.ts +0 -195
- package/src/__tests__/model-field-acl.test.ts +0 -237
- package/src/__tests__/model-hooks.test.ts +0 -130
- package/src/__tests__/model-ref-resolution.test.ts +0 -268
- package/src/__tests__/model-roundtrip.test.ts +0 -502
- package/src/__tests__/model-runtime.test.ts +0 -112
- package/src/__tests__/model-transitions.test.ts +0 -183
- package/src/__tests__/nrt-action-trace.test.ts +0 -391
- package/src/__tests__/pipeline-hardening.test.ts +0 -413
- package/src/__tests__/project-compiler.test.ts +0 -546
- package/src/__tests__/project-decompiler.test.ts +0 -343
- package/src/__tests__/query-compilation.test.ts +0 -145
- package/src/__tests__/round-trip/PLAN.md +0 -158
- package/src/__tests__/round-trip/README.md +0 -52
- package/src/__tests__/round-trip/RESULTS.md +0 -86
- package/src/__tests__/round-trip/fixtures/data-heavy/main.workflow.tsx +0 -55
- package/src/__tests__/round-trip/fixtures/data-heavy/mm.config.ts +0 -11
- package/src/__tests__/round-trip/fixtures/data-heavy/models/contact.ts +0 -54
- package/src/__tests__/round-trip/fixtures/full-workflow/main.workflow.tsx +0 -79
- package/src/__tests__/round-trip/fixtures/full-workflow/mm.config.ts +0 -12
- package/src/__tests__/round-trip/fixtures/full-workflow/models/order.ts +0 -50
- package/src/__tests__/round-trip/fixtures/simple-crud/main.workflow.tsx +0 -25
- package/src/__tests__/round-trip/fixtures/simple-crud/mm.config.ts +0 -11
- package/src/__tests__/round-trip/fixtures/simple-crud/models/task.ts +0 -32
- package/src/__tests__/round-trip/fixtures/view-heavy/main.workflow.tsx +0 -79
- package/src/__tests__/round-trip/fixtures/view-heavy/mm.config.ts +0 -10
- package/src/__tests__/round-trip/round-trip.test.ts +0 -2598
- package/src/__tests__/round-trip-ir.test.ts +0 -300
- package/src/__tests__/round-trip.test.ts +0 -1212
- package/src/__tests__/route-merging.test.ts +0 -372
- package/src/__tests__/router-composition.test.ts +0 -489
- package/src/__tests__/router-extractor.test.ts +0 -176
- package/src/__tests__/server-action-extractor.test.ts +0 -128
- package/src/__tests__/smart-type-inference.test.ts +0 -365
- package/src/__tests__/source-envelope.test.ts +0 -284
- package/src/__tests__/source-fidelity.test.ts +0 -516
- package/src/__tests__/state-extractor.test.ts +0 -115
- package/src/__tests__/strict-mode.test.ts +0 -227
- package/src/__tests__/transition-effect-extractor.test.ts +0 -119
- package/src/__tests__/transition-extractor.test.ts +0 -68
- package/src/__tests__/ts-to-expression.test.ts +0 -462
- package/src/__tests__/type-generator.test.ts +0 -201
- package/src/__tests__/uber-validation.test.ts +0 -502
- package/src/action-compiler.ts +0 -361
- package/src/babel/emitters/experience-transform.ts +0 -199
- package/src/babel/emitters/ir-to-tsx-emitter.ts +0 -110
- package/src/babel/emitters/pure-form-emitter.ts +0 -1023
- package/src/babel/emitters/runtime-glue-emitter.ts +0 -39
- package/src/babel/extractors/change-extractor.ts +0 -199
- package/src/babel/extractors/component-extractor.ts +0 -907
- package/src/babel/extractors/computed-extractor.ts +0 -262
- package/src/babel/extractors/context-extractor.ts +0 -277
- package/src/babel/extractors/during-extractor.ts +0 -295
- package/src/babel/extractors/effect-extractor.ts +0 -340
- package/src/babel/extractors/event-extractor.ts +0 -235
- package/src/babel/extractors/grammar-island-extractor.ts +0 -302
- package/src/babel/extractors/model-extractor.ts +0 -1018
- package/src/babel/extractors/router-extractor.ts +0 -303
- package/src/babel/extractors/server-action-extractor.ts +0 -173
- package/src/babel/extractors/server-action-hook-extractor.ts +0 -72
- package/src/babel/extractors/server-state-extractor.ts +0 -88
- package/src/babel/extractors/state-extractor.ts +0 -214
- package/src/babel/extractors/transition-effect-extractor.ts +0 -176
- package/src/babel/extractors/transition-extractor.ts +0 -143
- package/src/babel/index.ts +0 -24
- package/src/babel/transpilers/ts-to-expression.ts +0 -674
- package/src/babel/visitor.ts +0 -807
- package/src/cli/auth.ts +0 -255
- package/src/cli/build.ts +0 -288
- package/src/cli/deploy.ts +0 -206
- package/src/cli/index.ts +0 -328
- package/src/cli/installer.ts +0 -261
- package/src/cli/lock-file.ts +0 -94
- package/src/cli/mmrc.ts +0 -22
- package/src/cli/pull.ts +0 -172
- package/src/cli/registry-client.ts +0 -175
- package/src/cli/test.ts +0 -397
- package/src/cli/type-generator.ts +0 -243
- package/src/codemod/__tests__/forward.test.ts +0 -239
- package/src/codemod/__tests__/reverse.test.ts +0 -145
- package/src/codemod/__tests__/round-trip.test.ts +0 -137
- package/src/codemod/annotation.ts +0 -97
- package/src/codemod/classify.ts +0 -197
- package/src/codemod/cli.ts +0 -207
- package/src/codemod/control-flow.ts +0 -409
- package/src/codemod/forward.ts +0 -244
- package/src/codemod/import-manager.ts +0 -171
- package/src/codemod/index.ts +0 -120
- package/src/codemod/reverse.ts +0 -197
- package/src/codemod/rules.ts +0 -174
- package/src/codemod/state-transform.ts +0 -126
- package/src/decompiler/ast-builder.ts +0 -538
- package/src/decompiler/config-generator.ts +0 -151
- package/src/decompiler/index.ts +0 -315
- package/src/decompiler/project-decompiler.ts +0 -1776
- package/src/decompiler/project.ts +0 -862
- package/src/decompiler/split-strategy.ts +0 -140
- package/src/decompiler/state-emitter.ts +0 -1053
- package/src/decompiler/sx-emitter.ts +0 -318
- package/src/decompiler/workspace-hydrator.ts +0 -189
- package/src/dev-server.ts +0 -238
- package/src/envelope/fs-tree.ts +0 -217
- package/src/envelope/source-envelope.ts +0 -264
- package/src/envelope.ts +0 -315
- package/src/incremental-compiler.ts +0 -401
- package/src/index.ts +0 -99
- package/src/model-compiler.ts +0 -277
- package/src/project-compiler.ts +0 -1629
- package/src/route-extractor.ts +0 -333
- package/src/testing/index.ts +0 -32
- package/src/testing/snapshot.ts +0 -252
- package/src/testing/test-utils.ts +0 -226
- package/src/types.ts +0 -68
- package/src/vite/index.ts +0 -288
- package/test-compile.mjs +0 -142
- package/tsconfig.json +0 -25
- package/tsup.config.ts +0 -23
- package/vitest.config.ts +0 -9
|
@@ -1,1221 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Full Blueprint Coverage Test — compiles every example blueprint end-to-end
|
|
3
|
-
* and asserts on fields, states, transitions, experience tree structure,
|
|
4
|
-
* data sources, roles, routes, and component hierarchy.
|
|
5
|
-
*
|
|
6
|
-
* Covers both single-file examples (examples/) and real package-level blueprints
|
|
7
|
-
* (packages/blueprint-*).
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { describe, it, expect, beforeAll } from 'vitest';
|
|
11
|
-
import { transformSync } from '@babel/core';
|
|
12
|
-
import { readFileSync, readdirSync, existsSync } from 'fs';
|
|
13
|
-
import { resolve, join } from 'path';
|
|
14
|
-
import babelPlugin from '../babel';
|
|
15
|
-
import { compileProject } from '../project-compiler';
|
|
16
|
-
import type { IRWorkflowDefinition, IRExperienceNode } from '@mindmatrix/player-core';
|
|
17
|
-
|
|
18
|
-
// =============================================================================
|
|
19
|
-
// Helpers
|
|
20
|
-
// =============================================================================
|
|
21
|
-
|
|
22
|
-
function compileFile(relativePath: string): IRWorkflowDefinition {
|
|
23
|
-
const filePath = resolve(__dirname, '../../examples', relativePath);
|
|
24
|
-
const code = readFileSync(filePath, 'utf-8');
|
|
25
|
-
const result = transformSync(code, {
|
|
26
|
-
filename: filePath,
|
|
27
|
-
plugins: [[babelPlugin]],
|
|
28
|
-
parserOpts: { plugins: ['typescript', 'jsx'], attachComment: true },
|
|
29
|
-
});
|
|
30
|
-
const ir = (result as any)?.metadata?.mindmatrixIR;
|
|
31
|
-
if (!ir) throw new Error(`No IR produced for ${relativePath}`);
|
|
32
|
-
return ir;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function loadProjectFiles(dir: string): Record<string, string> {
|
|
36
|
-
const files: Record<string, string> = {};
|
|
37
|
-
|
|
38
|
-
function walk(current: string, prefix: string) {
|
|
39
|
-
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
40
|
-
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
41
|
-
if (entry.isDirectory()) {
|
|
42
|
-
// Skip test directories and node_modules
|
|
43
|
-
if (entry.name === 'tests' || entry.name === 'node_modules' || entry.name === '__tests__') continue;
|
|
44
|
-
walk(join(current, entry.name), rel);
|
|
45
|
-
} else if (
|
|
46
|
-
entry.name.endsWith('.ts') ||
|
|
47
|
-
entry.name.endsWith('.tsx')
|
|
48
|
-
) {
|
|
49
|
-
files[rel] = readFileSync(join(current, entry.name), 'utf-8');
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
walk(dir, '');
|
|
55
|
-
return files;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function loadExampleProjectFiles(dir: string): Record<string, string> {
|
|
59
|
-
const base = resolve(__dirname, '../../examples', dir);
|
|
60
|
-
return loadProjectFiles(base);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function getExperience(ir: IRWorkflowDefinition): IRExperienceNode | undefined {
|
|
64
|
-
return ir.metadata?.experience as IRExperienceNode | undefined;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** Recursively collect all component types from an experience tree */
|
|
68
|
-
function collectComponents(node: IRExperienceNode | undefined): string[] {
|
|
69
|
-
if (!node) return [];
|
|
70
|
-
const result: string[] = [];
|
|
71
|
-
if (node.component) result.push(node.component);
|
|
72
|
-
if (node.children) {
|
|
73
|
-
for (const child of node.children) {
|
|
74
|
-
result.push(...collectComponents(child));
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return result;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** Recursively find all nodes matching a component name */
|
|
81
|
-
function findNodes(node: IRExperienceNode | undefined, component: string): IRExperienceNode[] {
|
|
82
|
-
if (!node) return [];
|
|
83
|
-
const result: IRExperienceNode[] = [];
|
|
84
|
-
if (node.component === component) result.push(node);
|
|
85
|
-
if (node.children) {
|
|
86
|
-
for (const child of node.children) {
|
|
87
|
-
result.push(...findNodes(child, component));
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
return result;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** Find all nodes with bindings */
|
|
94
|
-
function findBoundNodes(node: IRExperienceNode | undefined): IRExperienceNode[] {
|
|
95
|
-
if (!node) return [];
|
|
96
|
-
const result: IRExperienceNode[] = [];
|
|
97
|
-
if (node.bindings && Object.keys(node.bindings).length > 0) result.push(node);
|
|
98
|
-
if (node.children) {
|
|
99
|
-
for (const child of node.children) {
|
|
100
|
-
result.push(...findBoundNodes(child));
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
return result;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/** Find all nodes with visible_when conditions */
|
|
107
|
-
function findConditionalNodes(node: IRExperienceNode | undefined): IRExperienceNode[] {
|
|
108
|
-
if (!node) return [];
|
|
109
|
-
const result: IRExperienceNode[] = [];
|
|
110
|
-
if (node.visible_when) result.push(node);
|
|
111
|
-
if (node.children) {
|
|
112
|
-
for (const child of node.children) {
|
|
113
|
-
result.push(...findConditionalNodes(child));
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
return result;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/** Count total nodes in tree */
|
|
120
|
-
function countNodes(node: IRExperienceNode | undefined): number {
|
|
121
|
-
if (!node) return 0;
|
|
122
|
-
let count = 1;
|
|
123
|
-
if (node.children) {
|
|
124
|
-
for (const child of node.children) {
|
|
125
|
-
count += countNodes(child);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
return count;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/** Collect all unique component types from experience tree */
|
|
132
|
-
function collectComponentTypes(node: IRExperienceNode | undefined): Set<string> {
|
|
133
|
-
const types = new Set<string>();
|
|
134
|
-
if (!node) return types;
|
|
135
|
-
function walk(n: IRExperienceNode) {
|
|
136
|
-
if (n.component) types.add(n.component);
|
|
137
|
-
n.children?.forEach(walk);
|
|
138
|
-
}
|
|
139
|
-
walk(node);
|
|
140
|
-
return types;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// =============================================================================
|
|
144
|
-
// Single-File Blueprints (from examples/)
|
|
145
|
-
// =============================================================================
|
|
146
|
-
|
|
147
|
-
describe('Full Blueprint Coverage: counter.workflow.tsx', () => {
|
|
148
|
-
let ir: IRWorkflowDefinition;
|
|
149
|
-
let exp: IRExperienceNode | undefined;
|
|
150
|
-
|
|
151
|
-
beforeAll(() => {
|
|
152
|
-
ir = compileFile('counter.workflow.tsx');
|
|
153
|
-
exp = getExperience(ir);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it('compiles with correct metadata', () => {
|
|
157
|
-
expect(ir.slug).toBe('counter');
|
|
158
|
-
expect(ir.version).toBe('1.0.0');
|
|
159
|
-
expect(ir.category).toBe('workflow');
|
|
160
|
-
expect(ir.description).toContain('simple counter');
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it('extracts exactly 3 fields with correct types', () => {
|
|
164
|
-
expect(ir.fields).toHaveLength(3);
|
|
165
|
-
const count = ir.fields.find(f => f.name === 'count');
|
|
166
|
-
const status = ir.fields.find(f => f.name === 'status');
|
|
167
|
-
const lastAction = ir.fields.find(f => f.name === 'lastAction');
|
|
168
|
-
expect(count).toBeDefined();
|
|
169
|
-
expect(count!.type).toBe('number');
|
|
170
|
-
expect(count!.default_value).toBe(0);
|
|
171
|
-
expect(status).toBeDefined();
|
|
172
|
-
expect(status!.type).toBe('text');
|
|
173
|
-
expect(status!.default_value).toBe('idle');
|
|
174
|
-
expect(lastAction).toBeDefined();
|
|
175
|
-
expect(lastAction!.type).toBe('text');
|
|
176
|
-
expect(lastAction!.default_value).toBe('');
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it('extracts 4 transitions with correct from/to', () => {
|
|
180
|
-
expect(ir.transitions).toHaveLength(4);
|
|
181
|
-
const start = ir.transitions.find(t => t.name === 'start');
|
|
182
|
-
const inc = ir.transitions.find(t => t.name === 'increment');
|
|
183
|
-
const dec = ir.transitions.find(t => t.name === 'decrement');
|
|
184
|
-
const reset = ir.transitions.find(t => t.name === 'reset');
|
|
185
|
-
expect(start).toBeDefined();
|
|
186
|
-
expect(start!.to).toBe('counting');
|
|
187
|
-
expect(inc).toBeDefined();
|
|
188
|
-
expect(inc!.from).toContain('counting');
|
|
189
|
-
expect(inc!.to).toBe('counting');
|
|
190
|
-
expect(dec).toBeDefined();
|
|
191
|
-
expect(dec!.from).toContain('counting');
|
|
192
|
-
expect(dec!.to).toBe('counting');
|
|
193
|
-
expect(reset).toBeDefined();
|
|
194
|
-
expect(reset!.from).toContain('counting');
|
|
195
|
-
expect(reset!.to).toBe('idle');
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it('extracts states with on_enter hooks', () => {
|
|
199
|
-
const counting = ir.states.find(s => s.name === 'counting');
|
|
200
|
-
expect(counting).toBeDefined();
|
|
201
|
-
expect(counting!.on_enter.length).toBeGreaterThanOrEqual(1);
|
|
202
|
-
const setFieldActions = counting!.on_enter.filter(a => a.type === 'set_field');
|
|
203
|
-
expect(setFieldActions.length).toBeGreaterThanOrEqual(1);
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it('extracts field watchers from useOnChange', () => {
|
|
207
|
-
const watchers = (ir.metadata as any)?.fieldWatchers;
|
|
208
|
-
expect(watchers).toBeDefined();
|
|
209
|
-
expect(watchers.length).toBeGreaterThanOrEqual(1);
|
|
210
|
-
const countWatcher = watchers.find((w: any) => w.field === 'count');
|
|
211
|
-
expect(countWatcher).toBeDefined();
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it('produces an experience tree with root Stack', () => {
|
|
215
|
-
expect(exp).toBeDefined();
|
|
216
|
-
expect(exp!.component).toBe('Stack');
|
|
217
|
-
expect(exp!.className).toBe('counter-app');
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
it('experience tree contains Heading with counter binding', () => {
|
|
221
|
-
const headings = findNodes(exp, 'Text');
|
|
222
|
-
const counterHeading = headings.find(h =>
|
|
223
|
-
h.bindings?.value?.includes('count') || h.config?.value?.toString().includes('Counter')
|
|
224
|
-
);
|
|
225
|
-
expect(counterHeading).toBeDefined();
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
it('experience tree contains 3 Button nodes (+1, -1, Reset)', () => {
|
|
229
|
-
const buttons = findNodes(exp, 'Button');
|
|
230
|
-
expect(buttons.length).toBeGreaterThanOrEqual(3);
|
|
231
|
-
const labels = buttons.map(b => b.config?.label || b.config?.value).filter(Boolean);
|
|
232
|
-
expect(labels).toContain('+1');
|
|
233
|
-
expect(labels).toContain('-1');
|
|
234
|
-
expect(labels).toContain('Reset');
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
it('buttons have onClick bindings', () => {
|
|
238
|
-
const buttons = findNodes(exp, 'Button');
|
|
239
|
-
for (const btn of buttons) {
|
|
240
|
-
const hasAction = btn.bindings?.onClick || btn.bindings?.onPress;
|
|
241
|
-
expect(hasAction).toBeDefined();
|
|
242
|
-
}
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
it('has no compilation errors', () => {
|
|
246
|
-
expect((ir.metadata as any)?.errors).toBeUndefined();
|
|
247
|
-
});
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
describe('Full Blueprint Coverage: todo-app.workflow.tsx', () => {
|
|
251
|
-
let ir: IRWorkflowDefinition;
|
|
252
|
-
let exp: IRExperienceNode | undefined;
|
|
253
|
-
|
|
254
|
-
beforeAll(() => {
|
|
255
|
-
ir = compileFile('todo-app.workflow.tsx');
|
|
256
|
-
exp = getExperience(ir);
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
it('compiles with correct metadata', () => {
|
|
260
|
-
expect(ir.slug).toBe('todo-app');
|
|
261
|
-
expect(ir.version).toBe('1.0.0');
|
|
262
|
-
expect(ir.category).toBe('data');
|
|
263
|
-
expect(ir.description).toContain('Todo list');
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
it('extracts exactly 3 fields', () => {
|
|
267
|
-
expect(ir.fields).toHaveLength(3);
|
|
268
|
-
expect(ir.fields.find(f => f.name === 'title')?.type).toBe('text');
|
|
269
|
-
expect(ir.fields.find(f => f.name === 'title')?.default_value).toBe('');
|
|
270
|
-
expect(ir.fields.find(f => f.name === 'filter')?.type).toBe('text');
|
|
271
|
-
expect(ir.fields.find(f => f.name === 'filter')?.default_value).toBe('all');
|
|
272
|
-
expect(ir.fields.find(f => f.name === 'itemCount')?.type).toBe('number');
|
|
273
|
-
expect(ir.fields.find(f => f.name === 'itemCount')?.default_value).toBe(0);
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
it('extracts 2 data sources targeting todo-item', () => {
|
|
277
|
-
const sources = (ir.metadata as any)?.dataSources;
|
|
278
|
-
expect(sources).toBeDefined();
|
|
279
|
-
expect(sources).toHaveLength(2);
|
|
280
|
-
expect(sources[0].slug).toBe('todo-item');
|
|
281
|
-
expect(sources[0].pageSize).toBe(100);
|
|
282
|
-
expect(sources[0].sort).toBe('created_at');
|
|
283
|
-
expect(sources[1].slug).toBe('todo-item');
|
|
284
|
-
expect(sources[1].pageSize).toBe(50);
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
it('extracts mutation target: todo-item', () => {
|
|
288
|
-
const targets = (ir.metadata as any)?.mutationTargets;
|
|
289
|
-
expect(targets).toBeDefined();
|
|
290
|
-
expect(targets).toContain('todo-item');
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
it('extracts role dependencies: editor, admin', () => {
|
|
294
|
-
const roles = (ir.metadata as any)?.roleDependencies;
|
|
295
|
-
expect(roles).toBeDefined();
|
|
296
|
-
expect(roles).toContain('editor');
|
|
297
|
-
expect(roles).toContain('admin');
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
it('extracts 3 transitions (activate, archive, restore)', () => {
|
|
301
|
-
expect(ir.transitions).toHaveLength(3);
|
|
302
|
-
const names = ir.transitions.map(t => t.name);
|
|
303
|
-
expect(names).toContain('activate');
|
|
304
|
-
expect(names).toContain('archive');
|
|
305
|
-
expect(names).toContain('restore');
|
|
306
|
-
|
|
307
|
-
const activate = ir.transitions.find(t => t.name === 'activate');
|
|
308
|
-
expect(activate!.to).toBe('active');
|
|
309
|
-
const archive = ir.transitions.find(t => t.name === 'archive');
|
|
310
|
-
expect(archive!.from).toContain('active');
|
|
311
|
-
expect(archive!.to).toBe('archived');
|
|
312
|
-
const restore = ir.transitions.find(t => t.name === 'restore');
|
|
313
|
-
expect(restore!.from).toContain('archived');
|
|
314
|
-
expect(restore!.to).toBe('active');
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
it('extracts states with enter/exit/during hooks', () => {
|
|
318
|
-
const active = ir.states.find(s => s.name === 'active');
|
|
319
|
-
expect(active).toBeDefined();
|
|
320
|
-
expect(active!.on_enter.length).toBeGreaterThanOrEqual(1);
|
|
321
|
-
expect(active!.on_exit.length).toBeGreaterThanOrEqual(1);
|
|
322
|
-
if (active!.during) {
|
|
323
|
-
expect(active!.during.length).toBeGreaterThanOrEqual(1);
|
|
324
|
-
expect(active!.during[0].interval_ms).toBe(30000);
|
|
325
|
-
}
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
it('experience tree has root Stack with className', () => {
|
|
329
|
-
expect(exp).toBeDefined();
|
|
330
|
-
expect(exp!.component).toBe('Stack');
|
|
331
|
-
expect(exp!.className).toBe('todo-app');
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
it('experience tree contains heading with item count binding', () => {
|
|
335
|
-
const textNodes = findNodes(exp, 'Text');
|
|
336
|
-
const heading = textNodes.find(t =>
|
|
337
|
-
t.bindings?.value?.includes('itemCount') ||
|
|
338
|
-
t.config?.value?.toString().includes('Todo List')
|
|
339
|
-
);
|
|
340
|
-
expect(heading).toBeDefined();
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
it('experience tree contains Input with placeholder', () => {
|
|
344
|
-
const inputs = findNodes(exp, 'TextInput');
|
|
345
|
-
const todoInput = inputs.find(i =>
|
|
346
|
-
i.config?.placeholder === 'What needs to be done?'
|
|
347
|
-
);
|
|
348
|
-
expect(todoInput).toBeDefined();
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
it('experience tree contains 3 filter buttons (All, Active, Done)', () => {
|
|
352
|
-
const buttons = findNodes(exp, 'Button');
|
|
353
|
-
const filterLabels = buttons
|
|
354
|
-
.map(b => b.config?.label || b.config?.value)
|
|
355
|
-
.filter(Boolean);
|
|
356
|
-
expect(filterLabels).toContain('All');
|
|
357
|
-
expect(filterLabels).toContain('Active');
|
|
358
|
-
expect(filterLabels).toContain('Done');
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
it('experience tree contains Each (list) node for todos', () => {
|
|
362
|
-
const eachNodes = findNodes(exp, 'Each');
|
|
363
|
-
expect(eachNodes.length).toBeGreaterThanOrEqual(1);
|
|
364
|
-
const todoList = eachNodes.find(e => e.bindings?.items || e.config?.items);
|
|
365
|
-
expect(todoList).toBeDefined();
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
it('experience tree has conditional nodes (role guards, empty state)', () => {
|
|
369
|
-
const conditionals = findConditionalNodes(exp);
|
|
370
|
-
expect(conditionals.length).toBeGreaterThanOrEqual(2);
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
it('has no compilation errors', () => {
|
|
374
|
-
expect((ir.metadata as any)?.errors).toBeUndefined();
|
|
375
|
-
});
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
describe('Full Blueprint Coverage: dashboard.workflow.tsx', () => {
|
|
379
|
-
let ir: IRWorkflowDefinition;
|
|
380
|
-
let exp: IRExperienceNode | undefined;
|
|
381
|
-
|
|
382
|
-
beforeAll(() => {
|
|
383
|
-
ir = compileFile('dashboard.workflow.tsx');
|
|
384
|
-
exp = getExperience(ir);
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
it('compiles with correct metadata', () => {
|
|
388
|
-
expect(ir.slug).toBe('dashboard');
|
|
389
|
-
expect(ir.version).toBe('1.0.0');
|
|
390
|
-
expect(ir.category).toBe('blueprint');
|
|
391
|
-
expect(ir.description).toContain('dashboard blueprint');
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
it('extracts exactly 8 fields with correct types and defaults', () => {
|
|
395
|
-
expect(ir.fields).toHaveLength(8);
|
|
396
|
-
const fieldMap = Object.fromEntries(ir.fields.map(f => [f.name, f]));
|
|
397
|
-
|
|
398
|
-
expect(fieldMap.activeRoute.type).toBe('text');
|
|
399
|
-
expect(fieldMap.activeRoute.default_value).toBe('/dashboard');
|
|
400
|
-
expect(fieldMap.sidebarOpen.type).toBe('boolean');
|
|
401
|
-
expect(fieldMap.sidebarOpen.default_value).toBe(true);
|
|
402
|
-
expect(fieldMap.searchQuery.type).toBe('text');
|
|
403
|
-
expect(fieldMap.searchQuery.default_value).toBe('');
|
|
404
|
-
expect(fieldMap.notificationCount.type).toBe('number');
|
|
405
|
-
expect(fieldMap.notificationCount.default_value).toBe(0);
|
|
406
|
-
expect(fieldMap.selectedRecordId.type).toBe('text');
|
|
407
|
-
expect(fieldMap.theme.type).toBe('text');
|
|
408
|
-
expect(fieldMap.theme.default_value).toBe('light');
|
|
409
|
-
expect(fieldMap.refreshInterval.type).toBe('number');
|
|
410
|
-
expect(fieldMap.refreshInterval.default_value).toBe(30000);
|
|
411
|
-
expect(fieldMap.lastRefreshed.type).toBe('text');
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
it('extracts 5 data sources with correct slugs and configs', () => {
|
|
415
|
-
const sources = (ir.metadata as any)?.dataSources;
|
|
416
|
-
expect(sources).toBeDefined();
|
|
417
|
-
expect(sources).toHaveLength(5);
|
|
418
|
-
const slugs = sources.map((s: any) => s.slug);
|
|
419
|
-
expect(slugs).toContain('dashboard-metric');
|
|
420
|
-
expect(slugs).toContain('activity-log');
|
|
421
|
-
expect(slugs).toContain('workflow-definition');
|
|
422
|
-
expect(slugs).toContain('notification');
|
|
423
|
-
expect(slugs).toContain('user-setting');
|
|
424
|
-
|
|
425
|
-
const metricSource = sources.find((s: any) => s.slug === 'dashboard-metric');
|
|
426
|
-
expect(metricSource.pageSize).toBe(20);
|
|
427
|
-
expect(metricSource.sort).toBe('priority');
|
|
428
|
-
|
|
429
|
-
const activitySource = sources.find((s: any) => s.slug === 'activity-log');
|
|
430
|
-
expect(activitySource.pageSize).toBe(50);
|
|
431
|
-
expect(activitySource.sort).toBe('created_at');
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
it('extracts 2 mutation targets', () => {
|
|
435
|
-
const targets = (ir.metadata as any)?.mutationTargets;
|
|
436
|
-
expect(targets).toBeDefined();
|
|
437
|
-
expect(targets).toContain('user-setting');
|
|
438
|
-
expect(targets).toContain('activity-log');
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
it('extracts 3 role dependencies', () => {
|
|
442
|
-
const roles = (ir.metadata as any)?.roleDependencies;
|
|
443
|
-
expect(roles).toBeDefined();
|
|
444
|
-
expect(roles).toContain('admin');
|
|
445
|
-
expect(roles).toContain('viewer');
|
|
446
|
-
expect(roles).toContain('editor');
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
it('extracts 7 transitions with correct state machine', () => {
|
|
450
|
-
expect(ir.transitions).toHaveLength(7);
|
|
451
|
-
const byName = Object.fromEntries(ir.transitions.map(t => [t.name, t]));
|
|
452
|
-
|
|
453
|
-
expect(byName.initialize.to).toBe('active');
|
|
454
|
-
expect(byName.suspend.from).toContain('active');
|
|
455
|
-
expect(byName.suspend.to).toBe('suspended');
|
|
456
|
-
expect(byName.resume.from).toContain('suspended');
|
|
457
|
-
expect(byName.resume.to).toBe('active');
|
|
458
|
-
expect(byName.deactivate.from).toContain('active');
|
|
459
|
-
expect(byName.deactivate.to).toBe('archived');
|
|
460
|
-
|
|
461
|
-
expect(byName.navigate.from).toContain('active');
|
|
462
|
-
expect(byName.navigate.to).toBe('active');
|
|
463
|
-
|
|
464
|
-
expect(byName['open-settings'].from).toContain('active');
|
|
465
|
-
expect(byName['open-settings'].to).toBe('settings');
|
|
466
|
-
expect(byName['close-settings'].from).toContain('settings');
|
|
467
|
-
expect(byName['close-settings'].to).toBe('active');
|
|
468
|
-
});
|
|
469
|
-
|
|
470
|
-
it('extracts states with on_enter, on_exit, and during', () => {
|
|
471
|
-
const active = ir.states.find(s => s.name === 'active');
|
|
472
|
-
expect(active).toBeDefined();
|
|
473
|
-
expect(active!.on_enter.length).toBeGreaterThanOrEqual(1);
|
|
474
|
-
expect(active!.on_exit.length).toBeGreaterThanOrEqual(1);
|
|
475
|
-
if (active!.during) {
|
|
476
|
-
expect(active!.during.length).toBeGreaterThanOrEqual(1);
|
|
477
|
-
expect(active!.during[0].interval_ms).toBe(30000);
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
const settings = ir.states.find(s => s.name === 'settings');
|
|
481
|
-
expect(settings).toBeDefined();
|
|
482
|
-
expect(settings!.on_enter.length).toBeGreaterThanOrEqual(1);
|
|
483
|
-
});
|
|
484
|
-
|
|
485
|
-
it('extracts field watchers for activeRoute and notificationCount', () => {
|
|
486
|
-
const watchers = (ir.metadata as any)?.fieldWatchers;
|
|
487
|
-
expect(watchers).toBeDefined();
|
|
488
|
-
expect(watchers.length).toBeGreaterThanOrEqual(2);
|
|
489
|
-
const watchedFields = watchers.map((w: any) => w.field);
|
|
490
|
-
expect(watchedFields).toContain('activeRoute');
|
|
491
|
-
expect(watchedFields).toContain('notificationCount');
|
|
492
|
-
});
|
|
493
|
-
|
|
494
|
-
it('extracts 2 event subscriptions', () => {
|
|
495
|
-
const events = ir.on_event;
|
|
496
|
-
expect(events).toBeDefined();
|
|
497
|
-
expect(events!.length).toBeGreaterThanOrEqual(2);
|
|
498
|
-
const matches = events!.map(e => e.match);
|
|
499
|
-
expect(matches).toContain('data:updated');
|
|
500
|
-
expect(matches).toContain('notification:received');
|
|
501
|
-
});
|
|
502
|
-
|
|
503
|
-
it('experience tree has root Stack', () => {
|
|
504
|
-
expect(exp).toBeDefined();
|
|
505
|
-
expect(exp!.component).toBe('Stack');
|
|
506
|
-
expect(exp!.className).toBe('dashboard-app');
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
it('experience tree contains MetricCard components', () => {
|
|
510
|
-
const cards = findNodes(exp, 'MetricCard');
|
|
511
|
-
expect(cards.length).toBeGreaterThanOrEqual(1);
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
it('experience tree contains Chart components (line and bar)', () => {
|
|
515
|
-
const charts = findNodes(exp, 'Chart');
|
|
516
|
-
expect(charts.length).toBeGreaterThanOrEqual(2);
|
|
517
|
-
const types = charts.map(c => c.config?.type);
|
|
518
|
-
expect(types).toContain('line');
|
|
519
|
-
expect(types).toContain('bar');
|
|
520
|
-
});
|
|
521
|
-
|
|
522
|
-
it('experience tree contains ServerGrid for data browser', () => {
|
|
523
|
-
const grids = findNodes(exp, 'ServerGrid');
|
|
524
|
-
expect(grids.length).toBeGreaterThanOrEqual(1);
|
|
525
|
-
const grid = grids[0];
|
|
526
|
-
expect(grid.config?.endpoint).toBe('/api/v1/workflow/definitions');
|
|
527
|
-
expect(grid.config?.pageSize).toBe(25);
|
|
528
|
-
const columns = grid.config?.columns as any[];
|
|
529
|
-
expect(columns).toBeDefined();
|
|
530
|
-
expect(columns.length).toBe(4);
|
|
531
|
-
expect(columns.map((c: any) => c.field)).toEqual(['name', 'category', 'version', 'updated_at']);
|
|
532
|
-
});
|
|
533
|
-
|
|
534
|
-
it('experience tree contains Select for theme and refresh rate', () => {
|
|
535
|
-
const selects = findNodes(exp, 'Select');
|
|
536
|
-
expect(selects.length).toBeGreaterThanOrEqual(2);
|
|
537
|
-
const themeSelect = selects.find(s =>
|
|
538
|
-
s.config?.bind === 'theme' || s.bindings?.bind === 'theme'
|
|
539
|
-
);
|
|
540
|
-
expect(themeSelect).toBeDefined();
|
|
541
|
-
expect(themeSelect!.config?.options).toEqual(['light', 'dark', 'system']);
|
|
542
|
-
});
|
|
543
|
-
|
|
544
|
-
it('experience tree contains ScrollArea', () => {
|
|
545
|
-
const scrollAreas = findNodes(exp, 'ScrollArea');
|
|
546
|
-
expect(scrollAreas.length).toBeGreaterThanOrEqual(1);
|
|
547
|
-
});
|
|
548
|
-
|
|
549
|
-
it('experience tree has conditional rendering for admin sections', () => {
|
|
550
|
-
const conditionals = findConditionalNodes(exp);
|
|
551
|
-
expect(conditionals.length).toBeGreaterThanOrEqual(3);
|
|
552
|
-
});
|
|
553
|
-
|
|
554
|
-
it('experience tree has Each loops for metrics, activities, notifications', () => {
|
|
555
|
-
const eachNodes = findNodes(exp, 'Each');
|
|
556
|
-
expect(eachNodes.length).toBeGreaterThanOrEqual(3);
|
|
557
|
-
});
|
|
558
|
-
|
|
559
|
-
it('experience tree has Section components in settings', () => {
|
|
560
|
-
const sections = findNodes(exp, 'Section');
|
|
561
|
-
expect(sections.length).toBeGreaterThanOrEqual(1);
|
|
562
|
-
const general = sections.find(s =>
|
|
563
|
-
s.config?.title === 'General' || s.config?.description?.toString().includes('dashboard preferences')
|
|
564
|
-
);
|
|
565
|
-
expect(general).toBeDefined();
|
|
566
|
-
});
|
|
567
|
-
|
|
568
|
-
it('experience tree contains Card components', () => {
|
|
569
|
-
const cards = findNodes(exp, 'Card');
|
|
570
|
-
expect(cards.length).toBeGreaterThanOrEqual(2);
|
|
571
|
-
});
|
|
572
|
-
|
|
573
|
-
it('experience tree contains Badge for notifications', () => {
|
|
574
|
-
const badges = findNodes(exp, 'Badge');
|
|
575
|
-
expect(badges.length).toBeGreaterThanOrEqual(1);
|
|
576
|
-
});
|
|
577
|
-
|
|
578
|
-
it('total node count is substantial (complex dashboard)', () => {
|
|
579
|
-
const total = countNodes(exp);
|
|
580
|
-
expect(total).toBeGreaterThan(30);
|
|
581
|
-
});
|
|
582
|
-
|
|
583
|
-
it('has data bindings referencing correct model slugs', () => {
|
|
584
|
-
const boundNodes = findBoundNodes(exp);
|
|
585
|
-
expect(boundNodes.length).toBeGreaterThan(5);
|
|
586
|
-
});
|
|
587
|
-
|
|
588
|
-
it('has no compilation errors', () => {
|
|
589
|
-
expect((ir.metadata as any)?.errors).toBeUndefined();
|
|
590
|
-
});
|
|
591
|
-
});
|
|
592
|
-
|
|
593
|
-
// =============================================================================
|
|
594
|
-
// Multi-File Example Blueprints (from examples/)
|
|
595
|
-
// =============================================================================
|
|
596
|
-
|
|
597
|
-
describe('Full Blueprint Coverage: authentication (multi-file)', () => {
|
|
598
|
-
let result: any;
|
|
599
|
-
let ir: IRWorkflowDefinition;
|
|
600
|
-
let exp: IRExperienceNode | undefined;
|
|
601
|
-
|
|
602
|
-
beforeAll(() => {
|
|
603
|
-
const files = loadExampleProjectFiles('authentication');
|
|
604
|
-
result = compileProject(files);
|
|
605
|
-
ir = result.ir;
|
|
606
|
-
exp = getExperience(ir);
|
|
607
|
-
});
|
|
608
|
-
|
|
609
|
-
it('compiles without errors', () => {
|
|
610
|
-
expect(result.errors).toHaveLength(0);
|
|
611
|
-
});
|
|
612
|
-
|
|
613
|
-
it('resolves slug and metadata from JSDoc', () => {
|
|
614
|
-
expect(ir.slug).toBe('mod-authentication');
|
|
615
|
-
expect(ir.version).toBe('1.0.0');
|
|
616
|
-
expect(ir.category).toBe('module');
|
|
617
|
-
});
|
|
618
|
-
|
|
619
|
-
it('extracts config fields (appName, layout, showSignup, showForgotPassword, redirectPath)', () => {
|
|
620
|
-
const fieldNames = ir.fields.map(f => f.name);
|
|
621
|
-
expect(fieldNames).toContain('appName');
|
|
622
|
-
expect(fieldNames).toContain('layout');
|
|
623
|
-
expect(fieldNames).toContain('showSignup');
|
|
624
|
-
expect(fieldNames).toContain('showForgotPassword');
|
|
625
|
-
expect(fieldNames).toContain('redirectPath');
|
|
626
|
-
|
|
627
|
-
expect(ir.fields.find(f => f.name === 'appName')?.default_value).toBe('My App');
|
|
628
|
-
expect(ir.fields.find(f => f.name === 'showSignup')?.type).toBe('boolean');
|
|
629
|
-
expect(ir.fields.find(f => f.name === 'showSignup')?.default_value).toBe(true);
|
|
630
|
-
expect(ir.fields.find(f => f.name === 'layout')?.default_value).toBe('card');
|
|
631
|
-
});
|
|
632
|
-
|
|
633
|
-
it('extracts runtime fields (email, password, confirmPassword, etc.)', () => {
|
|
634
|
-
const fieldNames = ir.fields.map(f => f.name);
|
|
635
|
-
expect(fieldNames).toContain('email');
|
|
636
|
-
expect(fieldNames).toContain('password');
|
|
637
|
-
expect(fieldNames).toContain('confirmPassword');
|
|
638
|
-
expect(fieldNames).toContain('firstName');
|
|
639
|
-
expect(fieldNames).toContain('lastName');
|
|
640
|
-
expect(fieldNames).toContain('errorMessage');
|
|
641
|
-
});
|
|
642
|
-
|
|
643
|
-
it('extracts total of 11 fields', () => {
|
|
644
|
-
expect(ir.fields).toHaveLength(11);
|
|
645
|
-
});
|
|
646
|
-
|
|
647
|
-
it('extracts 8 auth transitions', () => {
|
|
648
|
-
expect(ir.transitions).toHaveLength(8);
|
|
649
|
-
const names = ir.transitions.map(t => t.name);
|
|
650
|
-
expect(names).toContain('login');
|
|
651
|
-
expect(names).toContain('login_success');
|
|
652
|
-
expect(names).toContain('login_error');
|
|
653
|
-
expect(names).toContain('signup');
|
|
654
|
-
expect(names).toContain('signup_success');
|
|
655
|
-
expect(names).toContain('signup_error');
|
|
656
|
-
expect(names).toContain('logout');
|
|
657
|
-
expect(names).toContain('retry');
|
|
658
|
-
});
|
|
659
|
-
|
|
660
|
-
it('login transition requires email and password', () => {
|
|
661
|
-
const login = ir.transitions.find(t => t.name === 'login');
|
|
662
|
-
expect(login).toBeDefined();
|
|
663
|
-
expect(login!.from).toContain('unauthenticated');
|
|
664
|
-
expect(login!.to).toBe('authenticating');
|
|
665
|
-
expect(login!.required_fields).toContain('email');
|
|
666
|
-
expect(login!.required_fields).toContain('password');
|
|
667
|
-
});
|
|
668
|
-
|
|
669
|
-
it('signup transition requires email, password, firstName, lastName', () => {
|
|
670
|
-
const signup = ir.transitions.find(t => t.name === 'signup');
|
|
671
|
-
expect(signup).toBeDefined();
|
|
672
|
-
expect(signup!.required_fields).toContain('email');
|
|
673
|
-
expect(signup!.required_fields).toContain('password');
|
|
674
|
-
expect(signup!.required_fields).toContain('firstName');
|
|
675
|
-
expect(signup!.required_fields).toContain('lastName');
|
|
676
|
-
});
|
|
677
|
-
|
|
678
|
-
it('extracts auth states (unauthenticated, authenticating, authenticated, error)', () => {
|
|
679
|
-
const stateNames = ir.states.map(s => s.name);
|
|
680
|
-
expect(stateNames).toContain('unauthenticated');
|
|
681
|
-
expect(stateNames).toContain('authenticating');
|
|
682
|
-
expect(stateNames).toContain('authenticated');
|
|
683
|
-
expect(stateNames).toContain('error');
|
|
684
|
-
});
|
|
685
|
-
|
|
686
|
-
it('states have on_enter hooks that clear fields', () => {
|
|
687
|
-
const unauth = ir.states.find(s => s.name === 'unauthenticated');
|
|
688
|
-
expect(unauth!.on_enter.length).toBeGreaterThanOrEqual(1);
|
|
689
|
-
|
|
690
|
-
const errorState = ir.states.find(s => s.name === 'error');
|
|
691
|
-
expect(errorState!.on_enter.length).toBeGreaterThanOrEqual(1);
|
|
692
|
-
|
|
693
|
-
const authenticated = ir.states.find(s => s.name === 'authenticated');
|
|
694
|
-
expect(authenticated!.on_enter.length).toBeGreaterThanOrEqual(1);
|
|
695
|
-
});
|
|
696
|
-
|
|
697
|
-
it('experience tree contains Route nodes for /login and /signup', () => {
|
|
698
|
-
const routes = findNodes(exp, 'Route');
|
|
699
|
-
expect(routes.length).toBeGreaterThanOrEqual(1);
|
|
700
|
-
const paths = routes.map(r => r.config?.path);
|
|
701
|
-
expect(paths).toContain('/login');
|
|
702
|
-
});
|
|
703
|
-
|
|
704
|
-
it('experience tree contains Show for conditional signup', () => {
|
|
705
|
-
const showNodes = findNodes(exp, 'Show');
|
|
706
|
-
expect(showNodes.length).toBeGreaterThanOrEqual(1);
|
|
707
|
-
});
|
|
708
|
-
|
|
709
|
-
it('experience tree contains TextInput for email and password', () => {
|
|
710
|
-
const inputs = findNodes(exp, 'TextInput');
|
|
711
|
-
expect(inputs.length).toBeGreaterThanOrEqual(2);
|
|
712
|
-
const binds = inputs.map(i => i.config?.bind || i.bindings?.bind);
|
|
713
|
-
expect(binds).toContain('email');
|
|
714
|
-
expect(binds).toContain('password');
|
|
715
|
-
});
|
|
716
|
-
|
|
717
|
-
it('experience tree contains Button for Sign In', () => {
|
|
718
|
-
const buttons = findNodes(exp, 'Button');
|
|
719
|
-
const labels = buttons.map(b => b.config?.label || b.config?.value).filter(Boolean);
|
|
720
|
-
expect(labels).toContain('Sign In');
|
|
721
|
-
});
|
|
722
|
-
|
|
723
|
-
it('experience tree contains Link for navigation between login/signup', () => {
|
|
724
|
-
const links = findNodes(exp, 'Link');
|
|
725
|
-
expect(links.length).toBeGreaterThanOrEqual(1);
|
|
726
|
-
const destinations = links.map(l => l.config?.to);
|
|
727
|
-
expect(destinations).toContain('/signup');
|
|
728
|
-
});
|
|
729
|
-
|
|
730
|
-
it('produces child definitions for pages', () => {
|
|
731
|
-
expect(result.childDefinitions).toBeDefined();
|
|
732
|
-
});
|
|
733
|
-
|
|
734
|
-
it('has no compilation errors', () => {
|
|
735
|
-
expect(result.errors).toHaveLength(0);
|
|
736
|
-
});
|
|
737
|
-
});
|
|
738
|
-
|
|
739
|
-
describe('Full Blueprint Coverage: invoice-approval (multi-file)', () => {
|
|
740
|
-
let result: any;
|
|
741
|
-
let ir: IRWorkflowDefinition;
|
|
742
|
-
let exp: IRExperienceNode | undefined;
|
|
743
|
-
|
|
744
|
-
beforeAll(() => {
|
|
745
|
-
const files = loadExampleProjectFiles('invoice-approval');
|
|
746
|
-
result = compileProject(files);
|
|
747
|
-
ir = result.ir;
|
|
748
|
-
exp = getExperience(ir);
|
|
749
|
-
});
|
|
750
|
-
|
|
751
|
-
it('compiles without errors', () => {
|
|
752
|
-
expect(result.errors).toHaveLength(0);
|
|
753
|
-
});
|
|
754
|
-
|
|
755
|
-
it('resolves blueprint config from mm.config.ts', () => {
|
|
756
|
-
expect(ir.slug).toBe('invoice-approval');
|
|
757
|
-
expect(ir.name).toBe('Invoice Approval');
|
|
758
|
-
expect(ir.version).toBe('1.0.0');
|
|
759
|
-
expect(ir.category).toBe('blueprint');
|
|
760
|
-
});
|
|
761
|
-
|
|
762
|
-
it('extracts fields from main workflow', () => {
|
|
763
|
-
const fieldNames = ir.fields.map(f => f.name);
|
|
764
|
-
expect(fieldNames).toContain('activeRoute');
|
|
765
|
-
expect(fieldNames).toContain('selectedInvoiceId');
|
|
766
|
-
expect(fieldNames).toContain('filterStatus');
|
|
767
|
-
expect(fieldNames).toContain('totalPending');
|
|
768
|
-
expect(fieldNames).toContain('lastAction');
|
|
769
|
-
});
|
|
770
|
-
|
|
771
|
-
it('extracts 4 approval transitions', () => {
|
|
772
|
-
expect(ir.transitions.length).toBeGreaterThanOrEqual(4);
|
|
773
|
-
const names = ir.transitions.map(t => t.name);
|
|
774
|
-
expect(names).toContain('submit');
|
|
775
|
-
expect(names).toContain('approve');
|
|
776
|
-
expect(names).toContain('reject');
|
|
777
|
-
expect(names).toContain('revise');
|
|
778
|
-
});
|
|
779
|
-
|
|
780
|
-
it('submit transition has requiredFields and roles', () => {
|
|
781
|
-
const submit = ir.transitions.find(t => t.name === 'submit');
|
|
782
|
-
expect(submit).toBeDefined();
|
|
783
|
-
expect(submit!.from).toContain('draft');
|
|
784
|
-
expect(submit!.to).toBe('submitted');
|
|
785
|
-
expect(submit!.required_fields).toContain('amount');
|
|
786
|
-
expect(submit!.required_fields).toContain('vendor');
|
|
787
|
-
expect(submit!.required_fields).toContain('date');
|
|
788
|
-
expect(submit!.roles).toContain('submitter');
|
|
789
|
-
});
|
|
790
|
-
|
|
791
|
-
it('approve/reject transitions restricted to approver role', () => {
|
|
792
|
-
const approve = ir.transitions.find(t => t.name === 'approve');
|
|
793
|
-
expect(approve!.roles).toContain('approver');
|
|
794
|
-
const reject = ir.transitions.find(t => t.name === 'reject');
|
|
795
|
-
expect(reject!.roles).toContain('approver');
|
|
796
|
-
});
|
|
797
|
-
|
|
798
|
-
it('extracts role dependencies', () => {
|
|
799
|
-
const roles = (ir.metadata as any)?.roleDependencies;
|
|
800
|
-
expect(roles).toBeDefined();
|
|
801
|
-
expect(roles).toContain('approver');
|
|
802
|
-
expect(roles).toContain('submitter');
|
|
803
|
-
});
|
|
804
|
-
|
|
805
|
-
it('extracts states with on_enter hooks', () => {
|
|
806
|
-
const draft = ir.states.find(s => s.name === 'draft');
|
|
807
|
-
expect(draft).toBeDefined();
|
|
808
|
-
expect(draft!.on_enter.length).toBeGreaterThanOrEqual(1);
|
|
809
|
-
|
|
810
|
-
const submitted = ir.states.find(s => s.name === 'submitted');
|
|
811
|
-
expect(submitted).toBeDefined();
|
|
812
|
-
expect(submitted!.on_enter.length).toBeGreaterThanOrEqual(1);
|
|
813
|
-
|
|
814
|
-
const approved = ir.states.find(s => s.name === 'approved');
|
|
815
|
-
expect(approved).toBeDefined();
|
|
816
|
-
expect(approved!.on_enter.length).toBeGreaterThanOrEqual(1);
|
|
817
|
-
});
|
|
818
|
-
|
|
819
|
-
it('experience tree has root Stack', () => {
|
|
820
|
-
expect(exp).toBeDefined();
|
|
821
|
-
expect(exp!.component).toBe('Stack');
|
|
822
|
-
});
|
|
823
|
-
|
|
824
|
-
it('experience tree contains heading "Invoice Approval"', () => {
|
|
825
|
-
const textNodes = findNodes(exp, 'Text');
|
|
826
|
-
const heading = textNodes.find(t =>
|
|
827
|
-
t.config?.value === 'Invoice Approval' ||
|
|
828
|
-
t.config?.value?.toString().includes('Invoice Approval')
|
|
829
|
-
);
|
|
830
|
-
expect(heading).toBeDefined();
|
|
831
|
-
});
|
|
832
|
-
|
|
833
|
-
it('experience tree contains Show nodes for route pages', () => {
|
|
834
|
-
const showNodes = findNodes(exp, 'Show');
|
|
835
|
-
expect(showNodes.length).toBeGreaterThanOrEqual(3);
|
|
836
|
-
});
|
|
837
|
-
|
|
838
|
-
it('produces child definitions for pages and model', () => {
|
|
839
|
-
expect(result.childDefinitions).toBeDefined();
|
|
840
|
-
});
|
|
841
|
-
|
|
842
|
-
it('has no compilation errors', () => {
|
|
843
|
-
expect(result.errors).toHaveLength(0);
|
|
844
|
-
});
|
|
845
|
-
});
|
|
846
|
-
|
|
847
|
-
describe('Full Blueprint Coverage: uber-app (multi-file)', () => {
|
|
848
|
-
let result: any;
|
|
849
|
-
let ir: IRWorkflowDefinition;
|
|
850
|
-
|
|
851
|
-
beforeAll(() => {
|
|
852
|
-
const files = loadExampleProjectFiles('uber-app');
|
|
853
|
-
result = compileProject(files);
|
|
854
|
-
ir = result.ir;
|
|
855
|
-
});
|
|
856
|
-
|
|
857
|
-
it('compiles without errors', () => {
|
|
858
|
-
expect(result.errors).toHaveLength(0);
|
|
859
|
-
});
|
|
860
|
-
|
|
861
|
-
it('resolves blueprint config from mm.config.ts', () => {
|
|
862
|
-
expect(ir.slug).toBe('uber-rideshare');
|
|
863
|
-
expect(ir.name).toBe('Uber Rideshare');
|
|
864
|
-
expect(ir.version).toBe('2.0.0');
|
|
865
|
-
expect(ir.category).toBe('blueprint');
|
|
866
|
-
});
|
|
867
|
-
|
|
868
|
-
it('compiles multiple files into fileIRs', () => {
|
|
869
|
-
const fileCount = Object.keys(result.fileIRs).length;
|
|
870
|
-
expect(fileCount).toBeGreaterThanOrEqual(5);
|
|
871
|
-
});
|
|
872
|
-
|
|
873
|
-
it('produces child definitions for models', () => {
|
|
874
|
-
expect(result.childDefinitions).toBeDefined();
|
|
875
|
-
if (result.childDefinitions.length > 0) {
|
|
876
|
-
const slugs = result.childDefinitions.map((d: any) => d.slug);
|
|
877
|
-
expect(slugs.length).toBeGreaterThanOrEqual(1);
|
|
878
|
-
}
|
|
879
|
-
});
|
|
880
|
-
|
|
881
|
-
it('merges fields from all workflow files', () => {
|
|
882
|
-
expect(ir.fields.length).toBeGreaterThanOrEqual(1);
|
|
883
|
-
});
|
|
884
|
-
|
|
885
|
-
it('merges transitions from all workflow files', () => {
|
|
886
|
-
expect(ir.transitions.length).toBeGreaterThanOrEqual(1);
|
|
887
|
-
});
|
|
888
|
-
|
|
889
|
-
it('has experience tree', () => {
|
|
890
|
-
const exp = getExperience(ir);
|
|
891
|
-
expect(result.errors).toHaveLength(0);
|
|
892
|
-
});
|
|
893
|
-
|
|
894
|
-
it('has no compilation errors', () => {
|
|
895
|
-
expect(result.errors).toHaveLength(0);
|
|
896
|
-
});
|
|
897
|
-
});
|
|
898
|
-
|
|
899
|
-
// =============================================================================
|
|
900
|
-
// Package-Level Blueprints (packages/blueprint-*)
|
|
901
|
-
// =============================================================================
|
|
902
|
-
|
|
903
|
-
const PACKAGE_BLUEPRINTS: Array<{
|
|
904
|
-
dir: string;
|
|
905
|
-
slug: string;
|
|
906
|
-
name: string;
|
|
907
|
-
minModels: number;
|
|
908
|
-
hasRoutes?: boolean;
|
|
909
|
-
hasActions?: boolean;
|
|
910
|
-
}> = [
|
|
911
|
-
{
|
|
912
|
-
dir: 'blueprint-employee-salary-tracking',
|
|
913
|
-
slug: 'employee-salary-tracking',
|
|
914
|
-
name: 'Employee Salary Tracking',
|
|
915
|
-
minModels: 4,
|
|
916
|
-
hasActions: false,
|
|
917
|
-
},
|
|
918
|
-
{
|
|
919
|
-
dir: 'blueprint-project-tracker',
|
|
920
|
-
slug: 'project-tracker',
|
|
921
|
-
name: 'Project Tracker',
|
|
922
|
-
minModels: 5,
|
|
923
|
-
hasRoutes: true,
|
|
924
|
-
hasActions: true,
|
|
925
|
-
},
|
|
926
|
-
{
|
|
927
|
-
dir: 'blueprint-auth',
|
|
928
|
-
slug: 'mod-authentication',
|
|
929
|
-
name: 'Authentication & Authorization',
|
|
930
|
-
minModels: 10,
|
|
931
|
-
hasActions: true,
|
|
932
|
-
},
|
|
933
|
-
{
|
|
934
|
-
dir: 'blueprint-chat',
|
|
935
|
-
slug: 'mm-chat',
|
|
936
|
-
name: 'MindMatrix Chat',
|
|
937
|
-
minModels: 8,
|
|
938
|
-
},
|
|
939
|
-
{
|
|
940
|
-
dir: 'blueprint-team-directory',
|
|
941
|
-
slug: 'team-directory',
|
|
942
|
-
name: 'Team Directory',
|
|
943
|
-
minModels: 1,
|
|
944
|
-
},
|
|
945
|
-
{
|
|
946
|
-
dir: 'blueprint-accelerator',
|
|
947
|
-
slug: 'blueprint-accelerator',
|
|
948
|
-
name: 'Accelerator',
|
|
949
|
-
minModels: 4,
|
|
950
|
-
hasRoutes: true,
|
|
951
|
-
hasActions: true,
|
|
952
|
-
},
|
|
953
|
-
];
|
|
954
|
-
|
|
955
|
-
for (const bp of PACKAGE_BLUEPRINTS) {
|
|
956
|
-
const bpDir = resolve(__dirname, '../../../', bp.dir);
|
|
957
|
-
|
|
958
|
-
describe(`Blueprint Compilation: ${bp.dir}`, () => {
|
|
959
|
-
let result: ReturnType<typeof compileProject>;
|
|
960
|
-
let ir: IRWorkflowDefinition;
|
|
961
|
-
let exp: IRExperienceNode | undefined;
|
|
962
|
-
let skipped = false;
|
|
963
|
-
|
|
964
|
-
beforeAll(() => {
|
|
965
|
-
if (!existsSync(join(bpDir, 'mm.config.ts'))) {
|
|
966
|
-
skipped = true;
|
|
967
|
-
return;
|
|
968
|
-
}
|
|
969
|
-
const files = loadProjectFiles(bpDir);
|
|
970
|
-
result = compileProject(files, { usePhase2Modules: true });
|
|
971
|
-
ir = result.ir;
|
|
972
|
-
exp = getExperience(ir);
|
|
973
|
-
});
|
|
974
|
-
|
|
975
|
-
it('directory and mm.config.ts exist', () => {
|
|
976
|
-
expect(existsSync(bpDir)).toBe(true);
|
|
977
|
-
expect(existsSync(join(bpDir, 'mm.config.ts'))).toBe(true);
|
|
978
|
-
});
|
|
979
|
-
|
|
980
|
-
it('compiles without errors', () => {
|
|
981
|
-
if (skipped) return;
|
|
982
|
-
expect(result.errors).toHaveLength(0);
|
|
983
|
-
});
|
|
984
|
-
|
|
985
|
-
it(`resolves slug to "${bp.slug}"`, () => {
|
|
986
|
-
if (skipped) return;
|
|
987
|
-
expect(ir.slug).toBe(bp.slug);
|
|
988
|
-
});
|
|
989
|
-
|
|
990
|
-
it(`resolves name to "${bp.name}"`, () => {
|
|
991
|
-
if (skipped) return;
|
|
992
|
-
expect(ir.name).toBe(bp.name);
|
|
993
|
-
});
|
|
994
|
-
|
|
995
|
-
it('has a version string', () => {
|
|
996
|
-
if (skipped) return;
|
|
997
|
-
expect(ir.version).toMatch(/^\d+\.\d+\.\d+$/);
|
|
998
|
-
});
|
|
999
|
-
|
|
1000
|
-
it('produces valid output with definitions', () => {
|
|
1001
|
-
if (skipped) return;
|
|
1002
|
-
expect(ir).toBeDefined();
|
|
1003
|
-
expect(ir.slug).toBeTruthy();
|
|
1004
|
-
expect(ir.version).toBeTruthy();
|
|
1005
|
-
});
|
|
1006
|
-
|
|
1007
|
-
it('compiles multiple files into fileIRs', () => {
|
|
1008
|
-
if (skipped) return;
|
|
1009
|
-
const fileCount = Object.keys(result.fileIRs).length;
|
|
1010
|
-
// At minimum: mm.config.ts + model files
|
|
1011
|
-
expect(fileCount).toBeGreaterThanOrEqual(2);
|
|
1012
|
-
});
|
|
1013
|
-
|
|
1014
|
-
it(`produces child definitions (at least ${bp.minModels} models declared)`, () => {
|
|
1015
|
-
if (skipped) return;
|
|
1016
|
-
// The config declares models — child definitions should be produced
|
|
1017
|
-
expect(result.childDefinitions).toBeDefined();
|
|
1018
|
-
});
|
|
1019
|
-
|
|
1020
|
-
it('produces valid experience tree', () => {
|
|
1021
|
-
if (skipped) return;
|
|
1022
|
-
// Multi-file blueprints may or may not have experience trees
|
|
1023
|
-
// depending on whether they have app/ pages
|
|
1024
|
-
if (exp) {
|
|
1025
|
-
expect(exp.component).toBeTruthy();
|
|
1026
|
-
// If the tree has children, verify structure
|
|
1027
|
-
if (exp.children && exp.children.length > 0) {
|
|
1028
|
-
for (const child of exp.children) {
|
|
1029
|
-
// Each child should have at minimum a component or id
|
|
1030
|
-
expect(child.component || child.id).toBeTruthy();
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
});
|
|
1035
|
-
|
|
1036
|
-
it('has expected model definitions in child definitions', () => {
|
|
1037
|
-
if (skipped) return;
|
|
1038
|
-
if (result.childDefinitions && result.childDefinitions.length > 0) {
|
|
1039
|
-
for (const child of result.childDefinitions) {
|
|
1040
|
-
// Each child should have a slug
|
|
1041
|
-
expect(child.slug).toBeTruthy();
|
|
1042
|
-
// Models should have fields
|
|
1043
|
-
if (child.fields) {
|
|
1044
|
-
for (const field of child.fields) {
|
|
1045
|
-
expect(field.name).toBeTruthy();
|
|
1046
|
-
expect(field.type).toBeTruthy();
|
|
1047
|
-
}
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
}
|
|
1051
|
-
});
|
|
1052
|
-
|
|
1053
|
-
it('fields have valid name and type', () => {
|
|
1054
|
-
if (skipped) return;
|
|
1055
|
-
for (const field of ir.fields) {
|
|
1056
|
-
expect(typeof field.name).toBe('string');
|
|
1057
|
-
expect(field.name.length).toBeGreaterThan(0);
|
|
1058
|
-
expect(typeof field.type).toBe('string');
|
|
1059
|
-
expect(field.type.length).toBeGreaterThan(0);
|
|
1060
|
-
}
|
|
1061
|
-
});
|
|
1062
|
-
|
|
1063
|
-
it('transitions reference valid state names', () => {
|
|
1064
|
-
if (skipped) return;
|
|
1065
|
-
if (ir.states.length > 0 && ir.transitions.length > 0) {
|
|
1066
|
-
const stateNames = new Set(ir.states.map(s => s.name));
|
|
1067
|
-
for (const t of ir.transitions) {
|
|
1068
|
-
expect(stateNames.has(t.to)).toBe(true);
|
|
1069
|
-
for (const from of t.from) {
|
|
1070
|
-
expect(stateNames.has(from)).toBe(true);
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
}
|
|
1074
|
-
});
|
|
1075
|
-
|
|
1076
|
-
it('no duplicate field names', () => {
|
|
1077
|
-
if (skipped) return;
|
|
1078
|
-
const names = ir.fields.map(f => f.name);
|
|
1079
|
-
expect(new Set(names).size).toBe(names.length);
|
|
1080
|
-
});
|
|
1081
|
-
|
|
1082
|
-
it('no duplicate transition names', () => {
|
|
1083
|
-
if (skipped) return;
|
|
1084
|
-
const names = ir.transitions.map(t => t.name);
|
|
1085
|
-
expect(new Set(names).size).toBe(names.length);
|
|
1086
|
-
});
|
|
1087
|
-
|
|
1088
|
-
if (bp.hasRoutes) {
|
|
1089
|
-
it('has valid route structure', () => {
|
|
1090
|
-
if (skipped) return;
|
|
1091
|
-
// Route table should be populated from mm.config.ts routes
|
|
1092
|
-
if (result.routeTable && result.routeTable.length > 0) {
|
|
1093
|
-
for (const route of result.routeTable) {
|
|
1094
|
-
expect(route.path).toBeTruthy();
|
|
1095
|
-
expect(route.path.startsWith('/')).toBe(true);
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
// Also check if Router/Route nodes exist in experience tree
|
|
1099
|
-
if (exp) {
|
|
1100
|
-
const routerNodes = findNodes(exp, 'Router');
|
|
1101
|
-
const routeNodes = findNodes(exp, 'Route');
|
|
1102
|
-
// At least one routing construct should exist
|
|
1103
|
-
expect(routerNodes.length + routeNodes.length).toBeGreaterThanOrEqual(0);
|
|
1104
|
-
}
|
|
1105
|
-
});
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
if (bp.hasActions) {
|
|
1109
|
-
it('has server actions compiled', () => {
|
|
1110
|
-
if (skipped) return;
|
|
1111
|
-
if (result.serverActions) {
|
|
1112
|
-
expect(result.serverActions.length).toBeGreaterThanOrEqual(1);
|
|
1113
|
-
for (const action of result.serverActions) {
|
|
1114
|
-
expect(action.name).toBeTruthy();
|
|
1115
|
-
}
|
|
1116
|
-
}
|
|
1117
|
-
});
|
|
1118
|
-
}
|
|
1119
|
-
});
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
// =============================================================================
|
|
1123
|
-
// Cross-Blueprint Structural Integrity
|
|
1124
|
-
// =============================================================================
|
|
1125
|
-
|
|
1126
|
-
describe('Cross-Blueprint Structural Integrity', () => {
|
|
1127
|
-
const singleFileBlueprints = [
|
|
1128
|
-
'counter.workflow.tsx',
|
|
1129
|
-
'todo-app.workflow.tsx',
|
|
1130
|
-
'dashboard.workflow.tsx',
|
|
1131
|
-
];
|
|
1132
|
-
|
|
1133
|
-
for (const file of singleFileBlueprints) {
|
|
1134
|
-
it(`${file}: every transition references valid states`, () => {
|
|
1135
|
-
const ir = compileFile(file);
|
|
1136
|
-
const stateNames = new Set(ir.states.map(s => s.name));
|
|
1137
|
-
|
|
1138
|
-
for (const t of ir.transitions) {
|
|
1139
|
-
expect(stateNames.has(t.to)).toBe(true);
|
|
1140
|
-
for (const from of t.from) {
|
|
1141
|
-
expect(stateNames.has(from)).toBe(true);
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1144
|
-
});
|
|
1145
|
-
|
|
1146
|
-
it(`${file}: no duplicate field names`, () => {
|
|
1147
|
-
const ir = compileFile(file);
|
|
1148
|
-
const names = ir.fields.map(f => f.name);
|
|
1149
|
-
expect(new Set(names).size).toBe(names.length);
|
|
1150
|
-
});
|
|
1151
|
-
|
|
1152
|
-
it(`${file}: no duplicate transition names`, () => {
|
|
1153
|
-
const ir = compileFile(file);
|
|
1154
|
-
const names = ir.transitions.map(t => t.name);
|
|
1155
|
-
expect(new Set(names).size).toBe(names.length);
|
|
1156
|
-
});
|
|
1157
|
-
|
|
1158
|
-
it(`${file}: experience tree has valid node IDs`, () => {
|
|
1159
|
-
const ir = compileFile(file);
|
|
1160
|
-
const exp = getExperience(ir);
|
|
1161
|
-
if (exp) {
|
|
1162
|
-
const ids: string[] = [];
|
|
1163
|
-
function collectIds(node: IRExperienceNode) {
|
|
1164
|
-
if (node.id) ids.push(node.id);
|
|
1165
|
-
node.children?.forEach(collectIds);
|
|
1166
|
-
}
|
|
1167
|
-
collectIds(exp);
|
|
1168
|
-
expect(ids.every(id => typeof id === 'string' && id.length > 0)).toBe(true);
|
|
1169
|
-
}
|
|
1170
|
-
});
|
|
1171
|
-
|
|
1172
|
-
it(`${file}: all field types are valid slug strings`, () => {
|
|
1173
|
-
const ir = compileFile(file);
|
|
1174
|
-
for (const field of ir.fields) {
|
|
1175
|
-
expect(typeof field.type).toBe('string');
|
|
1176
|
-
expect(field.type.length).toBeGreaterThan(0);
|
|
1177
|
-
}
|
|
1178
|
-
});
|
|
1179
|
-
}
|
|
1180
|
-
});
|
|
1181
|
-
|
|
1182
|
-
// =============================================================================
|
|
1183
|
-
// Cross-Blueprint: Package-level compilation sanity
|
|
1184
|
-
// =============================================================================
|
|
1185
|
-
|
|
1186
|
-
describe('Cross-Blueprint: All package blueprints compile', () => {
|
|
1187
|
-
const allBlueprintDirs = PACKAGE_BLUEPRINTS.map(bp => ({
|
|
1188
|
-
dir: resolve(__dirname, '../../../', bp.dir),
|
|
1189
|
-
name: bp.dir,
|
|
1190
|
-
}));
|
|
1191
|
-
|
|
1192
|
-
it('all 6 package blueprints have mm.config.ts', () => {
|
|
1193
|
-
for (const { dir, name } of allBlueprintDirs) {
|
|
1194
|
-
expect(existsSync(join(dir, 'mm.config.ts'))).toBe(true);
|
|
1195
|
-
}
|
|
1196
|
-
});
|
|
1197
|
-
|
|
1198
|
-
it('all package blueprints compile with zero errors', () => {
|
|
1199
|
-
for (const { dir, name } of allBlueprintDirs) {
|
|
1200
|
-
const files = loadProjectFiles(dir);
|
|
1201
|
-
const result = compileProject(files, { usePhase2Modules: true });
|
|
1202
|
-
expect(result.errors).toHaveLength(0);
|
|
1203
|
-
}
|
|
1204
|
-
});
|
|
1205
|
-
|
|
1206
|
-
it('all package blueprints produce non-empty slug', () => {
|
|
1207
|
-
for (const { dir, name } of allBlueprintDirs) {
|
|
1208
|
-
const files = loadProjectFiles(dir);
|
|
1209
|
-
const result = compileProject(files, { usePhase2Modules: true });
|
|
1210
|
-
expect(result.ir.slug).toBeTruthy();
|
|
1211
|
-
}
|
|
1212
|
-
});
|
|
1213
|
-
|
|
1214
|
-
it('all package blueprints produce at least 1 fileIR', () => {
|
|
1215
|
-
for (const { dir, name } of allBlueprintDirs) {
|
|
1216
|
-
const files = loadProjectFiles(dir);
|
|
1217
|
-
const result = compileProject(files, { usePhase2Modules: true });
|
|
1218
|
-
expect(Object.keys(result.fileIRs).length).toBeGreaterThanOrEqual(1);
|
|
1219
|
-
}
|
|
1220
|
-
});
|
|
1221
|
-
});
|