@specverse/engines 4.1.30 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/examples/manifests/frontend-only.yaml +3 -6
- package/assets/examples/manifests/fullstack-app.yaml +5 -7
- package/assets/examples/manifests/fullstack-monorepo.yaml +3 -6
- package/dist/inference/comprehensive-engine.d.ts.map +1 -1
- package/dist/inference/comprehensive-engine.js +3 -19
- package/dist/inference/comprehensive-engine.js.map +1 -1
- package/dist/inference/core/rule-engine.d.ts +31 -0
- package/dist/inference/core/rule-engine.d.ts.map +1 -1
- package/dist/inference/core/rule-engine.js +117 -33
- package/dist/inference/core/rule-engine.js.map +1 -1
- package/dist/inference/core/rule-file-types.d.ts +0 -2
- package/dist/inference/core/rule-file-types.d.ts.map +1 -1
- package/dist/inference/core/rule-file-types.js +3 -6
- package/dist/inference/core/rule-file-types.js.map +1 -1
- package/dist/inference/core/rule-loader.d.ts +5 -15
- package/dist/inference/core/rule-loader.d.ts.map +1 -1
- package/dist/inference/core/rule-loader.js +43 -132
- package/dist/inference/core/rule-loader.js.map +1 -1
- package/dist/inference/core/types.d.ts +0 -6
- package/dist/inference/core/types.d.ts.map +1 -1
- package/dist/inference/core/types.js +0 -4
- package/dist/inference/core/types.js.map +1 -1
- package/dist/inference/logical/generators/component-type-resolver.d.ts +0 -26
- package/dist/inference/logical/generators/component-type-resolver.d.ts.map +1 -1
- package/dist/inference/logical/generators/component-type-resolver.js +0 -19
- package/dist/inference/logical/generators/component-type-resolver.js.map +1 -1
- package/dist/inference/logical/generators/specialist-view-expander.d.ts +1 -17
- package/dist/inference/logical/generators/specialist-view-expander.d.ts.map +1 -1
- package/dist/inference/logical/generators/specialist-view-expander.js +0 -15
- package/dist/inference/logical/generators/specialist-view-expander.js.map +1 -1
- package/dist/inference/logical/generators/view-generator.d.ts +4 -14
- package/dist/inference/logical/generators/view-generator.d.ts.map +1 -1
- package/dist/inference/logical/generators/view-generator.js +6 -26
- package/dist/inference/logical/generators/view-generator.js.map +1 -1
- package/dist/inference/logical/index.d.ts +2 -2
- package/dist/inference/logical/index.d.ts.map +1 -1
- package/dist/inference/logical/logical-engine.d.ts.map +1 -1
- package/dist/inference/logical/logical-engine.js +17 -80
- package/dist/inference/logical/logical-engine.js.map +1 -1
- package/dist/inference/quint-transpiler.d.ts +5 -3
- package/dist/inference/quint-transpiler.d.ts.map +1 -1
- package/dist/inference/quint-transpiler.js +11 -6
- package/dist/inference/quint-transpiler.js.map +1 -1
- package/dist/libs/instance-factories/applications/templates/react-starter/app-tsx-generator.js +110 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/dashboard-body-composer.js +121 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/detail-body-composer.js +78 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/form-body-composer.js +190 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/helpers-emitter.js +45 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/html-to-jsx.js +192 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/list-body-composer.js +46 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/orchestrator.js +30 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/package-json-generator.js +38 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/regen-safety.js +89 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/view-emitter.js +56 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/views-generator.js +66 -0
- package/dist/libs/instance-factories/cli/templates/commander/command-generator.js +14 -11
- package/dist/realize/index.d.ts.map +1 -1
- package/dist/realize/index.js +15 -22
- package/dist/realize/index.js.map +1 -1
- package/dist/registry/utils/manifest-adapter.d.ts +8 -1
- package/dist/registry/utils/manifest-adapter.d.ts.map +1 -1
- package/dist/registry/utils/manifest-adapter.js +8 -1
- package/dist/registry/utils/manifest-adapter.js.map +1 -1
- package/libs/instance-factories/applications/react-app-starter.yaml +150 -0
- package/libs/instance-factories/applications/templates/react-starter/README.md +211 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/dashboard-body-composer.test.ts +153 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/detail-body-composer.test.ts +145 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/form-body-composer.test.ts +175 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/helpers-emitter.test.ts +55 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/html-to-jsx.test.ts +140 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/list-body-composer.test.ts +146 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/orchestrator.test.ts +163 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/parity-p2-factory-imports.test.ts +116 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/parity-p3-rendered-output.test.ts +183 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/regen-safety.test.ts +144 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/starter-generators.test.ts +114 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/view-emitter.test.ts +107 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/views-generator.test.ts +139 -0
- package/libs/instance-factories/applications/templates/react-starter/app-tsx-generator.ts +141 -0
- package/libs/instance-factories/applications/templates/react-starter/dashboard-body-composer.ts +174 -0
- package/libs/instance-factories/applications/templates/react-starter/detail-body-composer.ts +135 -0
- package/libs/instance-factories/applications/templates/react-starter/form-body-composer.ts +306 -0
- package/libs/instance-factories/applications/templates/react-starter/helpers-emitter.ts +60 -0
- package/libs/instance-factories/applications/templates/react-starter/html-to-jsx.ts +334 -0
- package/libs/instance-factories/applications/templates/react-starter/list-body-composer.ts +120 -0
- package/libs/instance-factories/applications/templates/react-starter/orchestrator.ts +80 -0
- package/libs/instance-factories/applications/templates/react-starter/package-json-generator.ts +57 -0
- package/libs/instance-factories/applications/templates/react-starter/regen-safety.ts +157 -0
- package/libs/instance-factories/applications/templates/react-starter/skeletons/dashboard.tsx.template +47 -0
- package/libs/instance-factories/applications/templates/react-starter/skeletons/detail.tsx.template +94 -0
- package/libs/instance-factories/applications/templates/react-starter/skeletons/form.tsx.template +114 -0
- package/libs/instance-factories/applications/templates/react-starter/skeletons/list.tsx.template +72 -0
- package/libs/instance-factories/applications/templates/react-starter/view-emitter.ts +151 -0
- package/libs/instance-factories/applications/templates/react-starter/views-generator.ts +137 -0
- package/libs/instance-factories/cli/templates/commander/command-generator.ts +14 -11
- package/package.json +3 -3
- package/dist/libs/instance-factories/applications/templates/react/_view-components-source.js +0 -530
- package/dist/libs/instance-factories/applications/templates/react/app-tsx-generator.js +0 -73
- package/dist/libs/instance-factories/applications/templates/react/field-helpers-generator.js +0 -99
- package/dist/libs/instance-factories/applications/templates/react/package-json-generator.js +0 -49
- package/dist/libs/instance-factories/applications/templates/react/pattern-adapter-generator.js +0 -156
- package/dist/libs/instance-factories/applications/templates/react/react-pattern-adapter.js +0 -935
- package/dist/libs/instance-factories/applications/templates/react/relationship-field-generator.js +0 -143
- package/dist/libs/instance-factories/applications/templates/react/tailwind-adapter-generator.js +0 -646
- package/dist/libs/instance-factories/applications/templates/react/tailwind-adapter-wrapper-generator.js +0 -65
- package/dist/libs/instance-factories/applications/templates/react/view-dashboard-generator.js +0 -143
- package/dist/libs/instance-factories/applications/templates/react/view-detail-generator.js +0 -143
- package/dist/libs/instance-factories/applications/templates/react/view-form-generator.js +0 -355
- package/dist/libs/instance-factories/applications/templates/react/view-list-generator.js +0 -91
- package/dist/libs/instance-factories/applications/templates/react/view-router-generator.js +0 -79
- package/dist/libs/instance-factories/views/index.js +0 -48
- package/dist/libs/instance-factories/views/templates/react/adapters/antd-adapter.js +0 -742
- package/dist/libs/instance-factories/views/templates/react/adapters/mui-adapter.js +0 -824
- package/dist/libs/instance-factories/views/templates/react/adapters/shadcn-adapter.js +0 -719
- package/dist/libs/instance-factories/views/templates/react/app-generator.js +0 -45
- package/dist/libs/instance-factories/views/templates/react/components-generator.js +0 -820
- package/dist/libs/instance-factories/views/templates/react/forms-generator.js +0 -275
- package/dist/libs/instance-factories/views/templates/react/frontend-package-json-generator.js +0 -46
- package/dist/libs/instance-factories/views/templates/react/hooks-generator.js +0 -81
- package/dist/libs/instance-factories/views/templates/react/index-css-generator.js +0 -9
- package/dist/libs/instance-factories/views/templates/react/index-html-generator.js +0 -23
- package/dist/libs/instance-factories/views/templates/react/main-tsx-generator.js +0 -21
- package/dist/libs/instance-factories/views/templates/react/react-component-generator.js +0 -299
- package/dist/libs/instance-factories/views/templates/react/router-generator.js +0 -136
- package/dist/libs/instance-factories/views/templates/react/router-generic-generator.js +0 -107
- package/dist/libs/instance-factories/views/templates/react/shared-utils-generator.js +0 -187
- package/dist/libs/instance-factories/views/templates/react/spec-json-generator.js +0 -7
- package/dist/libs/instance-factories/views/templates/react/types-generator.js +0 -56
- package/dist/libs/instance-factories/views/templates/react/views-metadata-generator.js +0 -27
- package/dist/libs/instance-factories/views/templates/react/vite-config-generator.js +0 -29
- package/dist/libs/instance-factories/views/templates/runtime/runtime-view-renderer.js +0 -261
- package/dist/libs/instance-factories/views/templates/shared/adapter-types.js +0 -34
- package/dist/libs/instance-factories/views/templates/shared/atomic-components-registry.js +0 -800
- package/dist/libs/instance-factories/views/templates/shared/base-generator.js +0 -305
- package/dist/libs/instance-factories/views/templates/shared/component-metadata.js +0 -517
- package/dist/libs/instance-factories/views/templates/shared/composite-pattern-types.js +0 -0
- package/dist/libs/instance-factories/views/templates/shared/composite-patterns.js +0 -445
- package/dist/libs/instance-factories/views/templates/shared/index.js +0 -80
- package/dist/libs/instance-factories/views/templates/shared/pattern-validator.js +0 -210
- package/dist/libs/instance-factories/views/templates/shared/property-mapper.js +0 -492
- package/dist/libs/instance-factories/views/templates/shared/syntax-mapper.js +0 -321
- package/dist/realize/index.js.bak +0 -758
- package/libs/instance-factories/applications/react-app.yaml +0 -186
- package/libs/instance-factories/applications/templates/react/_view-components-source.ts +0 -555
- package/libs/instance-factories/applications/templates/react/app-tsx-generator.ts +0 -94
- package/libs/instance-factories/applications/templates/react/field-helpers-generator.ts +0 -106
- package/libs/instance-factories/applications/templates/react/package-json-generator.ts +0 -57
- package/libs/instance-factories/applications/templates/react/pattern-adapter-generator.ts +0 -179
- package/libs/instance-factories/applications/templates/react/react-pattern-adapter.tsx +0 -1347
- package/libs/instance-factories/applications/templates/react/relationship-field-generator.ts +0 -150
- package/libs/instance-factories/applications/templates/react/tailwind-adapter-generator.ts +0 -704
- package/libs/instance-factories/applications/templates/react/tailwind-adapter-wrapper-generator.ts +0 -84
- package/libs/instance-factories/applications/templates/react/view-dashboard-generator.ts +0 -150
- package/libs/instance-factories/applications/templates/react/view-detail-generator.ts +0 -150
- package/libs/instance-factories/applications/templates/react/view-form-generator.ts +0 -362
- package/libs/instance-factories/applications/templates/react/view-list-generator.ts +0 -98
- package/libs/instance-factories/applications/templates/react/view-router-generator.ts +0 -89
- package/libs/instance-factories/views/README.md +0 -62
- package/libs/instance-factories/views/index.d.ts +0 -13
- package/libs/instance-factories/views/index.d.ts.map +0 -1
- package/libs/instance-factories/views/index.js +0 -18
- package/libs/instance-factories/views/index.js.map +0 -1
- package/libs/instance-factories/views/index.ts +0 -45
- package/libs/instance-factories/views/react-components.yaml +0 -129
- package/libs/instance-factories/views/templates/ARCHITECTURE.md +0 -198
- package/libs/instance-factories/views/templates/react/adapters/antd-adapter.ts +0 -869
- package/libs/instance-factories/views/templates/react/adapters/mui-adapter.ts +0 -953
- package/libs/instance-factories/views/templates/react/adapters/shadcn-adapter.ts +0 -806
- package/libs/instance-factories/views/templates/react/app-generator.ts +0 -55
- package/libs/instance-factories/views/templates/react/components-generator.ts +0 -938
- package/libs/instance-factories/views/templates/react/forms-generator.ts +0 -325
- package/libs/instance-factories/views/templates/react/frontend-package-json-generator.ts +0 -57
- package/libs/instance-factories/views/templates/react/hooks-generator.ts +0 -106
- package/libs/instance-factories/views/templates/react/index-css-generator.ts +0 -14
- package/libs/instance-factories/views/templates/react/index-html-generator.ts +0 -34
- package/libs/instance-factories/views/templates/react/main-tsx-generator.ts +0 -29
- package/libs/instance-factories/views/templates/react/react-component-generator.d.ts +0 -152
- package/libs/instance-factories/views/templates/react/react-component-generator.d.ts.map +0 -1
- package/libs/instance-factories/views/templates/react/react-component-generator.js +0 -398
- package/libs/instance-factories/views/templates/react/react-component-generator.js.map +0 -1
- package/libs/instance-factories/views/templates/react/react-component-generator.ts +0 -533
- package/libs/instance-factories/views/templates/react/router-generator.ts +0 -197
- package/libs/instance-factories/views/templates/react/router-generic-generator.ts +0 -132
- package/libs/instance-factories/views/templates/react/shared-utils-generator.ts +0 -196
- package/libs/instance-factories/views/templates/react/spec-json-generator.ts +0 -17
- package/libs/instance-factories/views/templates/react/types-generator.ts +0 -76
- package/libs/instance-factories/views/templates/react/views-metadata-generator.ts +0 -42
- package/libs/instance-factories/views/templates/react/vite-config-generator.ts +0 -38
- package/libs/instance-factories/views/templates/runtime/runtime-view-renderer.d.ts.map +0 -1
- package/libs/instance-factories/views/templates/runtime/runtime-view-renderer.js.map +0 -1
- package/libs/instance-factories/views/templates/runtime/runtime-view-renderer.ts +0 -474
- package/libs/instance-factories/views/templates/shared/__tests__/composite-patterns.test.ts +0 -242
- package/libs/instance-factories/views/templates/shared/adapter-types.d.ts +0 -77
- package/libs/instance-factories/views/templates/shared/adapter-types.d.ts.map +0 -1
- package/libs/instance-factories/views/templates/shared/adapter-types.js +0 -47
- package/libs/instance-factories/views/templates/shared/adapter-types.js.map +0 -1
- package/libs/instance-factories/views/templates/shared/adapter-types.ts +0 -142
- package/libs/instance-factories/views/templates/shared/atomic-components-registry.d.ts +0 -63
- package/libs/instance-factories/views/templates/shared/atomic-components-registry.d.ts.map +0 -1
- package/libs/instance-factories/views/templates/shared/atomic-components-registry.js +0 -822
- package/libs/instance-factories/views/templates/shared/atomic-components-registry.js.map +0 -1
- package/libs/instance-factories/views/templates/shared/atomic-components-registry.ts +0 -908
- package/libs/instance-factories/views/templates/shared/base-generator.d.ts +0 -247
- package/libs/instance-factories/views/templates/shared/base-generator.d.ts.map +0 -1
- package/libs/instance-factories/views/templates/shared/base-generator.js +0 -363
- package/libs/instance-factories/views/templates/shared/base-generator.js.map +0 -1
- package/libs/instance-factories/views/templates/shared/base-generator.ts +0 -608
- package/libs/instance-factories/views/templates/shared/component-metadata.d.ts +0 -254
- package/libs/instance-factories/views/templates/shared/component-metadata.d.ts.map +0 -1
- package/libs/instance-factories/views/templates/shared/component-metadata.js +0 -602
- package/libs/instance-factories/views/templates/shared/component-metadata.js.map +0 -1
- package/libs/instance-factories/views/templates/shared/component-metadata.ts +0 -803
- package/libs/instance-factories/views/templates/shared/composite-pattern-types.ts +0 -250
- package/libs/instance-factories/views/templates/shared/composite-patterns.ts +0 -535
- package/libs/instance-factories/views/templates/shared/index.ts +0 -68
- package/libs/instance-factories/views/templates/shared/pattern-validator.ts +0 -279
- package/libs/instance-factories/views/templates/shared/property-mapper.d.ts +0 -149
- package/libs/instance-factories/views/templates/shared/property-mapper.d.ts.map +0 -1
- package/libs/instance-factories/views/templates/shared/property-mapper.js +0 -580
- package/libs/instance-factories/views/templates/shared/property-mapper.js.map +0 -1
- package/libs/instance-factories/views/templates/shared/property-mapper.ts +0 -700
- package/libs/instance-factories/views/templates/shared/syntax-mapper.d.ts +0 -143
- package/libs/instance-factories/views/templates/shared/syntax-mapper.d.ts.map +0 -1
- package/libs/instance-factories/views/templates/shared/syntax-mapper.js +0 -420
- package/libs/instance-factories/views/templates/shared/syntax-mapper.js.map +0 -1
- package/libs/instance-factories/views/templates/shared/syntax-mapper.ts +0 -539
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* P3 — Rendered-output equivalence
|
|
3
|
+
*
|
|
4
|
+
* Architectural invariant: the same (spec, view, model) must produce
|
|
5
|
+
* semantically equivalent output in app-demo (runtime React adapter)
|
|
6
|
+
* and in ReactAppStarter (Factory B emitter). They differ only in
|
|
7
|
+
* execution model — one renders in the browser, the other emits
|
|
8
|
+
* static JSX — but the visible structure must match.
|
|
9
|
+
*
|
|
10
|
+
* This file tests the LIST VIEW path, where parity is most
|
|
11
|
+
* tractable because both consumers call the same Tailwind adapter
|
|
12
|
+
* for the table shell. The test covers:
|
|
13
|
+
*
|
|
14
|
+
* 1. Column selection — both use `inferFieldsFromSchema` from
|
|
15
|
+
* the canonical pattern engine. Drift in either composer's
|
|
16
|
+
* column-filter logic would diverge here.
|
|
17
|
+
* 2. Table shell HTML — the Tailwind adapter renders the same
|
|
18
|
+
* shell for both paths; ensured by Factory B importing
|
|
19
|
+
* `createUniversalTailwindAdapter` directly.
|
|
20
|
+
* 3. Field humanisation — `camelCase` → `Title Case` for headers.
|
|
21
|
+
*
|
|
22
|
+
* Not yet covered (future extensions):
|
|
23
|
+
* - Detail / form / dashboard views — would need a reference
|
|
24
|
+
* React-adapter render path captured as an HTML string, then
|
|
25
|
+
* diffed against the Factory B emitted JSX modulo structural
|
|
26
|
+
* differences. Doable but bigger test machinery.
|
|
27
|
+
* - FK display-name resolution — blocked on Factory B's TODO to
|
|
28
|
+
* replace plain-FK inputs with dropdowns.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { describe, it, expect } from 'vitest';
|
|
32
|
+
import { inferFieldsFromSchema } from '@specverse/runtime/views/core';
|
|
33
|
+
import { createUniversalTailwindAdapter } from '@specverse/runtime/views/tailwind';
|
|
34
|
+
import { composeListBody } from '../list-body-composer.js';
|
|
35
|
+
import type { EmitContext, ModelSpec } from '../view-emitter.js';
|
|
36
|
+
|
|
37
|
+
// ───────────────────────────────────────────────────────────────────────
|
|
38
|
+
// Reference model + view-spec setup
|
|
39
|
+
// ───────────────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
function makeReferenceModel(): ModelSpec {
|
|
42
|
+
return {
|
|
43
|
+
name: 'Article',
|
|
44
|
+
attributes: {
|
|
45
|
+
id: { type: 'UUID', required: true, auto: 'uuid4' }, // excluded
|
|
46
|
+
title: { type: 'String', required: true }, // included
|
|
47
|
+
slug: { type: 'String', required: true, unique: true }, // included
|
|
48
|
+
content: { type: 'Text', required: false }, // included
|
|
49
|
+
status: { type: 'String', values: ['draft', 'published'] },// included
|
|
50
|
+
authorId: { type: 'UUID', required: true }, // included (FK)
|
|
51
|
+
createdAt: { type: 'DateTime', auto: 'now' }, // excluded (auto)
|
|
52
|
+
updatedAt: { type: 'DateTime', auto: 'now' }, // excluded (auto)
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function makeContext(): EmitContext {
|
|
58
|
+
const model = makeReferenceModel();
|
|
59
|
+
return {
|
|
60
|
+
view: { type: 'list', model: 'Article' },
|
|
61
|
+
viewName: 'ArticleListView',
|
|
62
|
+
model,
|
|
63
|
+
modelSchemas: { Article: model },
|
|
64
|
+
renderBody: composeListBody,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function humanize(s: string): string {
|
|
69
|
+
return s.replace(/([A-Z])/g, ' $1').replace(/^./, c => c.toUpperCase()).trim();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ───────────────────────────────────────────────────────────────────────
|
|
73
|
+
// Parity assertions
|
|
74
|
+
// ───────────────────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
describe('P3 — list view column inference parity', () => {
|
|
77
|
+
it('runtime inferFieldsFromSchema and Factory B composer see the same columns', () => {
|
|
78
|
+
const ctx = makeContext();
|
|
79
|
+
const runtimeColumns = inferFieldsFromSchema(ctx.modelSchemas, ctx.model.name);
|
|
80
|
+
|
|
81
|
+
// We can extract Factory B's column choices by looking at what
|
|
82
|
+
// headers it emits into the rendered output.
|
|
83
|
+
const body = composeListBody(ctx);
|
|
84
|
+
|
|
85
|
+
for (const col of runtimeColumns) {
|
|
86
|
+
const label = humanize(col);
|
|
87
|
+
expect(body, `runtime column "${col}" (label: "${label}") must appear in Factory B output`)
|
|
88
|
+
.toContain(`>${label}<`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// And nothing EXTRA from the model that runtime excludes:
|
|
92
|
+
const runtimeSet = new Set(runtimeColumns);
|
|
93
|
+
const allAttrs = Object.keys(ctx.model.attributes);
|
|
94
|
+
const runtimeExcluded = allAttrs.filter(a => !runtimeSet.has(a));
|
|
95
|
+
for (const excluded of runtimeExcluded) {
|
|
96
|
+
const label = humanize(excluded);
|
|
97
|
+
// Factory B must NOT emit a <th> for this field.
|
|
98
|
+
const headerPattern = new RegExp(`<th\\b[^>]*>${label}</th>`);
|
|
99
|
+
expect(body).not.toMatch(headerPattern);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('matches the expected column set for the reference model', () => {
|
|
104
|
+
const cols = inferFieldsFromSchema(makeContext().modelSchemas, 'Article');
|
|
105
|
+
// Canonical expectation for this reference spec.
|
|
106
|
+
expect(cols).toEqual(['title', 'slug', 'content', 'status', 'authorId']);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('P3 — list view table shell parity', () => {
|
|
111
|
+
it('emits a table shell byte-identical to a direct Tailwind adapter call', () => {
|
|
112
|
+
// The composer calls createUniversalTailwindAdapter() + table.render()
|
|
113
|
+
// with a known columns list and a sentinel in children. If we call
|
|
114
|
+
// the adapter directly with the same args, we should get the same
|
|
115
|
+
// shell HTML (modulo the sentinel vs the Factory B JSX injection).
|
|
116
|
+
const ctx = makeContext();
|
|
117
|
+
const runtimeColumns = inferFieldsFromSchema(ctx.modelSchemas, ctx.model.name);
|
|
118
|
+
const headers = runtimeColumns.map(humanize);
|
|
119
|
+
|
|
120
|
+
const adapter = createUniversalTailwindAdapter({ darkMode: true });
|
|
121
|
+
const SENTINEL = 'SENTINEL';
|
|
122
|
+
const shellHtml = adapter.components.table.render({
|
|
123
|
+
properties: { columns: headers },
|
|
124
|
+
children: SENTINEL,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Every column header appears in the shell, in order.
|
|
128
|
+
let lastIdx = -1;
|
|
129
|
+
for (const header of headers) {
|
|
130
|
+
const idx = shellHtml.indexOf(`>${header}</th>`);
|
|
131
|
+
expect(idx, `header "${header}" must appear in Tailwind adapter output`).toBeGreaterThan(-1);
|
|
132
|
+
expect(idx, `header "${header}" must appear AFTER the previous header`).toBeGreaterThan(lastIdx);
|
|
133
|
+
lastIdx = idx;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Factory B's composer output must preserve the same header order.
|
|
137
|
+
const body = composeListBody(ctx);
|
|
138
|
+
let lastBodyIdx = -1;
|
|
139
|
+
for (const header of headers) {
|
|
140
|
+
const idx = body.indexOf(`>${header}</th>`);
|
|
141
|
+
expect(idx).toBeGreaterThan(-1);
|
|
142
|
+
expect(idx).toBeGreaterThan(lastBodyIdx);
|
|
143
|
+
lastBodyIdx = idx;
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('Factory B output carries Tailwind classes from the canonical adapter shell', () => {
|
|
148
|
+
const body = composeListBody(makeContext());
|
|
149
|
+
// A sampling of classes that come from the adapter's table template.
|
|
150
|
+
// If the adapter changes its classes, both consumers update together.
|
|
151
|
+
expect(body).toContain('overflow-x-auto');
|
|
152
|
+
expect(body).toContain('min-w-full');
|
|
153
|
+
expect(body).toContain('divide-y divide-gray-200');
|
|
154
|
+
expect(body).toContain('bg-gray-50 dark:bg-gray-800');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('rows are synthesized by Factory B (the adapter emits placeholder for react mount)', () => {
|
|
158
|
+
const body = composeListBody(makeContext());
|
|
159
|
+
// The body should contain a real JSX .map() — Factory B's
|
|
160
|
+
// contribution to the shared shell.
|
|
161
|
+
expect(body).toContain('filtered.map((item, idx) =>');
|
|
162
|
+
expect(body).toContain('onClick={() => onSelect?.(item)}');
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('P3 — field humanisation parity', () => {
|
|
167
|
+
it('both paths humanise column names the same way (camelCase → Title Case)', () => {
|
|
168
|
+
// Factory B's humanize is inlined in the composer. The runtime
|
|
169
|
+
// adapter humanises via the same-shaped regex in pattern-engine /
|
|
170
|
+
// react-pattern-adapter. This test documents the contract by
|
|
171
|
+
// comparing against a fixture.
|
|
172
|
+
const cases: [string, string][] = [
|
|
173
|
+
['title', 'Title'],
|
|
174
|
+
['authorId', 'Author Id'],
|
|
175
|
+
['createdAt', 'Created At'],
|
|
176
|
+
['slugifiedURL', 'Slugified U R L'],
|
|
177
|
+
['name', 'Name'],
|
|
178
|
+
];
|
|
179
|
+
for (const [input, expected] of cases) {
|
|
180
|
+
expect(humanize(input), `humanize("${input}")`).toBe(expected);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
});
|
package/libs/instance-factories/applications/templates/react-starter/__tests__/regen-safety.test.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync, mkdirSync } from 'fs';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import {
|
|
6
|
+
loadHashManifest,
|
|
7
|
+
saveHashManifest,
|
|
8
|
+
reconcileWrites,
|
|
9
|
+
sha256,
|
|
10
|
+
HASHES_DIR,
|
|
11
|
+
HASHES_FILE,
|
|
12
|
+
} from '../regen-safety.js';
|
|
13
|
+
|
|
14
|
+
let projectRoot: string;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
projectRoot = mkdtempSync(join(tmpdir(), 'specverse-regen-safety-'));
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
rmSync(projectRoot, { recursive: true, force: true });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('hash manifest persistence', () => {
|
|
25
|
+
it('loadHashManifest returns {} when the file is missing', () => {
|
|
26
|
+
expect(loadHashManifest(projectRoot)).toEqual({});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('round-trips through save → load', () => {
|
|
30
|
+
saveHashManifest(projectRoot, { 'src/a.ts': 'abc', 'src/b.ts': 'def' });
|
|
31
|
+
const loaded = loadHashManifest(projectRoot);
|
|
32
|
+
expect(loaded).toEqual({ 'src/a.ts': 'abc', 'src/b.ts': 'def' });
|
|
33
|
+
expect(existsSync(join(projectRoot, HASHES_DIR, HASHES_FILE))).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('tolerates a malformed manifest file by returning {}', () => {
|
|
37
|
+
const dir = join(projectRoot, HASHES_DIR);
|
|
38
|
+
mkdirSync(dir, { recursive: true });
|
|
39
|
+
writeFileSync(join(dir, HASHES_FILE), '{ not valid JSON', 'utf8');
|
|
40
|
+
expect(loadHashManifest(projectRoot)).toEqual({});
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('reconcileWrites — pure planning (no I/O writes)', () => {
|
|
45
|
+
it('approves brand-new files and records their hashes', () => {
|
|
46
|
+
const proposed = { 'src/views/PostListView.tsx': 'content-a' };
|
|
47
|
+
const result = reconcileWrites(projectRoot, proposed, {});
|
|
48
|
+
expect(result.approvedWrites).toEqual(proposed);
|
|
49
|
+
expect(result.skipped).toEqual([]);
|
|
50
|
+
expect(existsSync(join(projectRoot, 'src/views/PostListView.tsx'))).toBe(false);
|
|
51
|
+
expect(result.manifest['src/views/PostListView.tsx']).toBe(sha256('content-a'));
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('approves overwriting a pristine file and updates its hash', () => {
|
|
55
|
+
const path = 'src/a.ts';
|
|
56
|
+
const absPath = join(projectRoot, path);
|
|
57
|
+
const originalContent = 'v1';
|
|
58
|
+
const newContent = 'v2';
|
|
59
|
+
|
|
60
|
+
mkdirSync(join(projectRoot, 'src'), { recursive: true });
|
|
61
|
+
writeFileSync(absPath, originalContent, 'utf8');
|
|
62
|
+
const prevManifest = { [path]: sha256(originalContent) };
|
|
63
|
+
|
|
64
|
+
const result = reconcileWrites(projectRoot, { [path]: newContent }, prevManifest);
|
|
65
|
+
|
|
66
|
+
expect(result.approvedWrites[path]).toBe(newContent);
|
|
67
|
+
expect(result.skipped).toEqual([]);
|
|
68
|
+
// reconcileWrites is pure planning — filesystem unchanged
|
|
69
|
+
expect(readFileSync(absPath, 'utf8')).toBe(originalContent);
|
|
70
|
+
expect(result.manifest[path]).toBe(sha256(newContent));
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('skips a user-edited file and keeps the old hash record', () => {
|
|
74
|
+
const path = 'src/edited.ts';
|
|
75
|
+
const absPath = join(projectRoot, path);
|
|
76
|
+
const originalContent = 'v1';
|
|
77
|
+
const editedContent = 'v1 + user edits';
|
|
78
|
+
const newContent = 'v2';
|
|
79
|
+
|
|
80
|
+
mkdirSync(join(projectRoot, 'src'), { recursive: true });
|
|
81
|
+
writeFileSync(absPath, editedContent, 'utf8');
|
|
82
|
+
const prevManifest = { [path]: sha256(originalContent) };
|
|
83
|
+
|
|
84
|
+
const result = reconcileWrites(projectRoot, { [path]: newContent }, prevManifest);
|
|
85
|
+
|
|
86
|
+
expect(result.approvedWrites[path]).toBeUndefined();
|
|
87
|
+
expect(result.skipped).toHaveLength(1);
|
|
88
|
+
expect(result.skipped[0]).toMatchObject({
|
|
89
|
+
path,
|
|
90
|
+
reason: expect.stringContaining('edited'),
|
|
91
|
+
});
|
|
92
|
+
expect(readFileSync(absPath, 'utf8')).toBe(editedContent);
|
|
93
|
+
expect(result.manifest[path]).toBe(prevManifest[path]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('skips files that exist but have no prior hash record', () => {
|
|
97
|
+
const path = 'src/unknown.ts';
|
|
98
|
+
const absPath = join(projectRoot, path);
|
|
99
|
+
|
|
100
|
+
mkdirSync(join(projectRoot, 'src'), { recursive: true });
|
|
101
|
+
writeFileSync(absPath, 'manually placed', 'utf8');
|
|
102
|
+
|
|
103
|
+
const result = reconcileWrites(projectRoot, { [path]: 'would-overwrite' }, {});
|
|
104
|
+
|
|
105
|
+
expect(result.approvedWrites[path]).toBeUndefined();
|
|
106
|
+
expect(result.skipped).toHaveLength(1);
|
|
107
|
+
expect(result.skipped[0].reason).toContain('no prior hash recorded');
|
|
108
|
+
expect(readFileSync(absPath, 'utf8')).toBe('manually placed');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('does not mutate the input prevManifest', () => {
|
|
112
|
+
const prev = { 'a.ts': sha256('old') };
|
|
113
|
+
const prevSnapshot = { ...prev };
|
|
114
|
+
reconcileWrites(projectRoot, { 'a.ts': 'new', 'b.ts': 'also new' }, prev);
|
|
115
|
+
expect(prev).toEqual(prevSnapshot);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('handles a batch of mixed outcomes correctly', () => {
|
|
119
|
+
mkdirSync(join(projectRoot, 'src'), { recursive: true });
|
|
120
|
+
writeFileSync(join(projectRoot, 'src/pristine.ts'), 'v1', 'utf8');
|
|
121
|
+
writeFileSync(join(projectRoot, 'src/edited.ts'), 'v1 + user edit', 'utf8');
|
|
122
|
+
const prev = {
|
|
123
|
+
'src/pristine.ts': sha256('v1'),
|
|
124
|
+
'src/edited.ts': sha256('v1'),
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const proposed = {
|
|
128
|
+
'src/pristine.ts': 'v2',
|
|
129
|
+
'src/edited.ts': 'v2',
|
|
130
|
+
'src/new.ts': 'brand new',
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const result = reconcileWrites(projectRoot, proposed, prev);
|
|
134
|
+
|
|
135
|
+
expect(Object.keys(result.approvedWrites).sort()).toEqual(['src/new.ts', 'src/pristine.ts']);
|
|
136
|
+
expect(result.skipped.map(s => s.path)).toEqual(['src/edited.ts']);
|
|
137
|
+
// No writes happened
|
|
138
|
+
expect(readFileSync(join(projectRoot, 'src/pristine.ts'), 'utf8')).toBe('v1');
|
|
139
|
+
expect(readFileSync(join(projectRoot, 'src/edited.ts'), 'utf8')).toBe('v1 + user edit');
|
|
140
|
+
expect(result.manifest['src/pristine.ts']).toBe(sha256('v2'));
|
|
141
|
+
expect(result.manifest['src/edited.ts']).toBe(prev['src/edited.ts']);
|
|
142
|
+
expect(result.manifest['src/new.ts']).toBe(sha256('brand new'));
|
|
143
|
+
});
|
|
144
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import * as ts from 'typescript';
|
|
3
|
+
import { generate as generateAppTsx } from '../app-tsx-generator.js';
|
|
4
|
+
import { generate as generatePackageJson } from '../package-json-generator.js';
|
|
5
|
+
|
|
6
|
+
function assertValidTsx(source: string, label: string): void {
|
|
7
|
+
const result = ts.transpileModule(source, {
|
|
8
|
+
compilerOptions: {
|
|
9
|
+
jsx: ts.JsxEmit.Preserve,
|
|
10
|
+
target: ts.ScriptTarget.ES2022,
|
|
11
|
+
module: ts.ModuleKind.ESNext,
|
|
12
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
13
|
+
strict: false,
|
|
14
|
+
},
|
|
15
|
+
reportDiagnostics: true,
|
|
16
|
+
});
|
|
17
|
+
const errors = result.diagnostics?.filter(d => d.category === ts.DiagnosticCategory.Error) ?? [];
|
|
18
|
+
if (errors.length > 0) {
|
|
19
|
+
const message = errors
|
|
20
|
+
.map(d => ts.flattenDiagnosticMessageText(d.messageText, '\n'))
|
|
21
|
+
.join('\n');
|
|
22
|
+
throw new Error(`${label} failed to parse:\n${message}\n\n--- source ---\n${source}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('app-tsx-generator', () => {
|
|
27
|
+
it('produces valid TSX with one branch per model and view type', async () => {
|
|
28
|
+
const source = await generateAppTsx({
|
|
29
|
+
spec: {
|
|
30
|
+
models: {
|
|
31
|
+
Post: { name: 'Post', attributes: {} },
|
|
32
|
+
Author: { name: 'Author', attributes: {} },
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
assertValidTsx(source, 'App.tsx');
|
|
37
|
+
// One nav section per model
|
|
38
|
+
expect(source).toContain('Post');
|
|
39
|
+
expect(source).toContain('Author');
|
|
40
|
+
// One import per (model, view) pair — 2 models × 4 view types = 8 imports
|
|
41
|
+
const importLines = source.match(/^import \{ \w+View \} from '\.\/views\//gm) ?? [];
|
|
42
|
+
expect(importLines.length).toBe(8);
|
|
43
|
+
// Navigation wiring
|
|
44
|
+
expect(source).toContain("select('Post', 'list')");
|
|
45
|
+
expect(source).toContain("select('Author', 'dashboard')");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('handles an empty spec gracefully', async () => {
|
|
49
|
+
const source = await generateAppTsx({ spec: { models: {}, views: {} } });
|
|
50
|
+
assertValidTsx(source, 'App.tsx (empty spec)');
|
|
51
|
+
expect(source).toContain('No models in this spec.');
|
|
52
|
+
// Still renders a QueryClientProvider
|
|
53
|
+
expect(source).toContain('QueryClientProvider');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('uses @tanstack/react-query, not @specverse/runtime', async () => {
|
|
57
|
+
const source = await generateAppTsx({
|
|
58
|
+
spec: { models: { Post: { name: 'Post', attributes: {} } } },
|
|
59
|
+
});
|
|
60
|
+
expect(source).not.toContain('@specverse/runtime');
|
|
61
|
+
expect(source).toContain('@tanstack/react-query');
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('package-json-generator', () => {
|
|
66
|
+
it('omits @specverse/runtime from dependencies', async () => {
|
|
67
|
+
const json = await generatePackageJson({ spec: { name: 'test' } });
|
|
68
|
+
const pkg = JSON.parse(json);
|
|
69
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
70
|
+
expect(allDeps['@specverse/runtime']).toBeUndefined();
|
|
71
|
+
expect(allDeps['@specverse/engines']).toBeUndefined();
|
|
72
|
+
expect(allDeps['@specverse/types']).toBeUndefined();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('includes the deps the emitted views actually need', async () => {
|
|
76
|
+
const json = await generatePackageJson({ spec: { name: 'test' } });
|
|
77
|
+
const pkg = JSON.parse(json);
|
|
78
|
+
expect(pkg.dependencies).toMatchObject({
|
|
79
|
+
react: expect.any(String),
|
|
80
|
+
'react-dom': expect.any(String),
|
|
81
|
+
'@tanstack/react-query': expect.any(String),
|
|
82
|
+
});
|
|
83
|
+
expect(pkg.devDependencies).toMatchObject({
|
|
84
|
+
typescript: expect.any(String),
|
|
85
|
+
vite: expect.any(String),
|
|
86
|
+
tailwindcss: expect.any(String),
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('slugifies the app name', async () => {
|
|
91
|
+
const json = await generatePackageJson({
|
|
92
|
+
spec: { metadata: { name: 'My Fancy App!' } },
|
|
93
|
+
});
|
|
94
|
+
const pkg = JSON.parse(json);
|
|
95
|
+
expect(pkg.name).toBe('my-fancy-app');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('falls back to a default name when none is provided', async () => {
|
|
99
|
+
const json = await generatePackageJson({ spec: {} });
|
|
100
|
+
const pkg = JSON.parse(json);
|
|
101
|
+
expect(pkg.name).toBe('specverse-starter-app');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('parses as valid JSON with the expected shape', async () => {
|
|
105
|
+
const json = await generatePackageJson({ spec: { name: 'test' } });
|
|
106
|
+
const pkg = JSON.parse(json);
|
|
107
|
+
expect(pkg.type).toBe('module');
|
|
108
|
+
expect(pkg.scripts).toMatchObject({
|
|
109
|
+
dev: 'vite',
|
|
110
|
+
build: 'tsc && vite build',
|
|
111
|
+
preview: 'vite preview',
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
});
|
package/libs/instance-factories/applications/templates/react-starter/__tests__/view-emitter.test.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import * as ts from 'typescript';
|
|
3
|
+
import { emitView, type EmitContext, type ModelSpec, type ViewSpec } from '../view-emitter.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Build a minimal context. The `renderBody` is a stub that returns a
|
|
7
|
+
* fixed Tailwind table — the point of these tests is the emitter
|
|
8
|
+
* orchestration + substitutions + JSX validity, not the body's content.
|
|
9
|
+
* Parity with the real Tailwind adapter is covered by Phase 3 (P3).
|
|
10
|
+
*/
|
|
11
|
+
function makeContext(overrides: Partial<EmitContext> = {}): EmitContext {
|
|
12
|
+
const defaultModel: ModelSpec = {
|
|
13
|
+
name: 'Post',
|
|
14
|
+
attributes: {
|
|
15
|
+
id: { type: 'UUID', required: true },
|
|
16
|
+
title: { type: 'String', required: true },
|
|
17
|
+
body: { type: 'Text', required: false },
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const defaultView: ViewSpec = {
|
|
22
|
+
type: 'list',
|
|
23
|
+
model: 'Post',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
view: defaultView,
|
|
28
|
+
viewName: 'PostListView',
|
|
29
|
+
model: defaultModel,
|
|
30
|
+
modelSchemas: { Post: defaultModel },
|
|
31
|
+
// renderBody returns JSX-ready source that will be injected verbatim
|
|
32
|
+
// into {{BODY}}. NOT raw HTML — that's the composer's job.
|
|
33
|
+
renderBody: () =>
|
|
34
|
+
'<table className="w-full"><thead><tr><th className="px-4 py-2">Title</th></tr></thead>' +
|
|
35
|
+
'<tbody><tr><td className="px-4 py-2">Example</td></tr></tbody></table>',
|
|
36
|
+
...overrides,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function assertValidTsx(source: string, label: string): void {
|
|
41
|
+
const result = ts.transpileModule(source, {
|
|
42
|
+
compilerOptions: {
|
|
43
|
+
jsx: ts.JsxEmit.Preserve,
|
|
44
|
+
target: ts.ScriptTarget.ES2022,
|
|
45
|
+
module: ts.ModuleKind.ESNext,
|
|
46
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
47
|
+
strict: false,
|
|
48
|
+
},
|
|
49
|
+
reportDiagnostics: true,
|
|
50
|
+
});
|
|
51
|
+
const errors = result.diagnostics?.filter(d => d.category === ts.DiagnosticCategory.Error) ?? [];
|
|
52
|
+
if (errors.length > 0) {
|
|
53
|
+
const message = errors
|
|
54
|
+
.map(d => ts.flattenDiagnosticMessageText(d.messageText, '\n'))
|
|
55
|
+
.join('\n');
|
|
56
|
+
throw new Error(`${label} failed to parse as TSX:\n${message}\n\n--- source ---\n${source}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe('emitView — list', () => {
|
|
61
|
+
it('produces TSX that parses cleanly', () => {
|
|
62
|
+
const context = makeContext();
|
|
63
|
+
const source = emitView(context);
|
|
64
|
+
assertValidTsx(source, 'emitted list view');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('substitutes model names into the skeleton', () => {
|
|
68
|
+
const source = emitView(makeContext());
|
|
69
|
+
expect(source).toContain('PostListView'); // component name
|
|
70
|
+
expect(source).toContain('usePostsQuery'); // generated hook name
|
|
71
|
+
expect(source).toContain('useDeletePostMutation'); // generated hook name
|
|
72
|
+
expect(source).toContain("Search posts…"); // pluralized lowercase
|
|
73
|
+
expect(source).toContain('+ New Post'); // action button label
|
|
74
|
+
expect(source).toContain('No posts yet.'); // empty state
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('substitutes a non-trivial plural correctly', () => {
|
|
78
|
+
const m: ModelSpec = { name: 'Category', attributes: {} };
|
|
79
|
+
const source = emitView(makeContext({
|
|
80
|
+
model: m,
|
|
81
|
+
view: { type: 'list', model: 'Category' },
|
|
82
|
+
viewName: 'CategoryListView',
|
|
83
|
+
modelSchemas: { Category: m },
|
|
84
|
+
}));
|
|
85
|
+
expect(source).toContain('CategoryListView');
|
|
86
|
+
expect(source).toContain('useCategoriesQuery'); // "Category" → "Categories"
|
|
87
|
+
expect(source).toContain('Search categories…');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('injects the rendered body at {{BODY}}', () => {
|
|
91
|
+
const source = emitView(makeContext({
|
|
92
|
+
renderBody: () => '<table className="my-table"><tbody><tr><td>Custom</td></tr></tbody></table>',
|
|
93
|
+
}));
|
|
94
|
+
expect(source).toContain('<table className="my-table">');
|
|
95
|
+
expect(source).toContain('Custom');
|
|
96
|
+
// The placeholder itself should be gone.
|
|
97
|
+
expect(source).not.toContain('{{BODY}}');
|
|
98
|
+
// All other placeholders should be gone too.
|
|
99
|
+
expect(source).not.toMatch(/\{\{[A-Z_]+\}\}/);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('throws for an unknown view type', () => {
|
|
103
|
+
expect(() =>
|
|
104
|
+
emitView(makeContext({ view: { type: 'unknown-type-xyz' } }))
|
|
105
|
+
).toThrow(/No skeleton registered/);
|
|
106
|
+
});
|
|
107
|
+
});
|