@mmapp/react-compiler 0.1.0-alpha.1 → 0.1.0-alpha.5
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 +2814 -277
- 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-52XHYD2V.mjs +214 -0
- package/dist/chunk-5GUFFFGL.mjs +148 -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-EO6SYNCG.mjs +175 -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-J7JUAHS4.mjs +186 -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-NTB7OEX2.mjs +2918 -0
- package/dist/chunk-NW6555WJ.mjs +186 -0
- package/dist/chunk-OMZE6VLQ.mjs +214 -0
- package/dist/chunk-OPJKP747.mjs +7506 -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-THFYE5ZX.mjs +244 -0
- package/dist/chunk-VCAY2KGM.mjs +175 -0
- package/dist/chunk-WBYMW4NQ.mjs +3450 -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 +13189 -6838
- package/dist/cli/index.mjs +140 -22
- package/dist/codemod/cli.mjs +1 -1
- package/dist/codemod/index.mjs +1 -1
- package/dist/config-PL24KEWL.mjs +219 -0
- 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 +4135 -440
- package/dist/dev-server.mjs +5 -5
- package/dist/envelope.js +2812 -275
- package/dist/envelope.mjs +3 -3
- package/dist/index.d.mts +161 -2
- package/dist/index.d.ts +161 -2
- package/dist/index.js +4429 -428
- package/dist/index.mjs +217 -9
- package/{src/cli/init.ts → dist/init-7JQMAAXS.mjs} +70 -95
- package/dist/init-DQDX3QK6.mjs +369 -0
- 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-OP2VVGJQ.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-US7GAVIC.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/pull-P44LDRWB.mjs +109 -0
- package/dist/testing/index.js +2822 -285
- package/dist/testing/index.mjs +2 -2
- package/dist/verify-SEIXUGN4.mjs +1833 -0
- package/dist/vite/index.js +2815 -278
- package/dist/vite/index.mjs +3 -3
- package/examples/uber-app/app/admin/fleet.tsx +19 -19
- package/package.json +16 -6
- 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,237 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Model Field ACL Tests — validates field-level access control, computed fields, validation.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { describe, it, expect } from 'vitest';
|
|
6
|
-
import { transformSync } from '@babel/core';
|
|
7
|
-
import babelPlugin from '../babel';
|
|
8
|
-
import type { IRWorkflowDefinition } from '@mindmatrix/player-core';
|
|
9
|
-
|
|
10
|
-
function compileModel(code: string): IRWorkflowDefinition {
|
|
11
|
-
const result = transformSync(code, {
|
|
12
|
-
filename: 'models/test.ts',
|
|
13
|
-
plugins: [[babelPlugin, { mode: 'strict' }]],
|
|
14
|
-
parserOpts: { plugins: ['typescript'], attachComment: true },
|
|
15
|
-
});
|
|
16
|
-
return (result as any)?.metadata?.mindmatrixIR;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
describe('Model Field ACL', () => {
|
|
20
|
-
it('should apply visible_in_states', () => {
|
|
21
|
-
const ir = compileModel(`
|
|
22
|
-
export interface Item {
|
|
23
|
-
name: string;
|
|
24
|
-
balance: number;
|
|
25
|
-
status: 'draft' | 'active';
|
|
26
|
-
}
|
|
27
|
-
export const transitions = {};
|
|
28
|
-
export const fieldOptions = {
|
|
29
|
-
balance: { visible_in_states: ['active'] },
|
|
30
|
-
};
|
|
31
|
-
`);
|
|
32
|
-
|
|
33
|
-
const balance = ir.fields.find((f) => f.name === 'balance');
|
|
34
|
-
expect(balance!.visible_in_states).toEqual(['active']);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('should apply editable_in_states', () => {
|
|
38
|
-
const ir = compileModel(`
|
|
39
|
-
export interface Doc {
|
|
40
|
-
title: string;
|
|
41
|
-
status: 'draft' | 'published';
|
|
42
|
-
}
|
|
43
|
-
export const transitions = {};
|
|
44
|
-
export const fieldOptions = {
|
|
45
|
-
title: { editable_in_states: ['draft'] },
|
|
46
|
-
};
|
|
47
|
-
`);
|
|
48
|
-
|
|
49
|
-
const title = ir.fields.find((f) => f.name === 'title');
|
|
50
|
-
expect(title!.editable_in_states).toEqual(['draft']);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('should apply visible_to_roles', () => {
|
|
54
|
-
const ir = compileModel(`
|
|
55
|
-
export interface Account {
|
|
56
|
-
balance: number;
|
|
57
|
-
status: 'a' | 'b';
|
|
58
|
-
}
|
|
59
|
-
export const transitions = {};
|
|
60
|
-
export const fieldOptions = {
|
|
61
|
-
balance: { visible_to_roles: ['admin', 'accountant'] },
|
|
62
|
-
};
|
|
63
|
-
`);
|
|
64
|
-
|
|
65
|
-
const balance = ir.fields.find((f) => f.name === 'balance');
|
|
66
|
-
expect(balance!.visible_to_roles).toEqual(['admin', 'accountant']);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('should apply editable_by_roles', () => {
|
|
70
|
-
const ir = compileModel(`
|
|
71
|
-
export interface Config {
|
|
72
|
-
setting: string;
|
|
73
|
-
status: 'a' | 'b';
|
|
74
|
-
}
|
|
75
|
-
export const transitions = {};
|
|
76
|
-
export const fieldOptions = {
|
|
77
|
-
setting: { editable_by_roles: ['admin'] },
|
|
78
|
-
};
|
|
79
|
-
`);
|
|
80
|
-
|
|
81
|
-
const setting = ir.fields.find((f) => f.name === 'setting');
|
|
82
|
-
expect(setting!.editable_by_roles).toEqual(['admin']);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('should apply visible_when expression', () => {
|
|
86
|
-
const ir = compileModel(`
|
|
87
|
-
export interface Toggle {
|
|
88
|
-
secret: string;
|
|
89
|
-
status: 'a' | 'b';
|
|
90
|
-
}
|
|
91
|
-
export const transitions = {};
|
|
92
|
-
export const fieldOptions = {
|
|
93
|
-
secret: { visible_when: '$user.role == "admin"' },
|
|
94
|
-
};
|
|
95
|
-
`);
|
|
96
|
-
|
|
97
|
-
const secret = ir.fields.find((f) => f.name === 'secret');
|
|
98
|
-
expect(secret!.visible_when).toBe('$user.role == "admin"');
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('should apply editable_when expression', () => {
|
|
102
|
-
const ir = compileModel(`
|
|
103
|
-
export interface Record {
|
|
104
|
-
content: string;
|
|
105
|
-
status: 'a' | 'b';
|
|
106
|
-
}
|
|
107
|
-
export const transitions = {};
|
|
108
|
-
export const fieldOptions = {
|
|
109
|
-
content: { editable_when: '$user.id == $instance.ownerId' },
|
|
110
|
-
};
|
|
111
|
-
`);
|
|
112
|
-
|
|
113
|
-
const content = ir.fields.find((f) => f.name === 'content');
|
|
114
|
-
expect(content!.editable_when).toBe('$user.id == $instance.ownerId');
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('should apply computed field expression', () => {
|
|
118
|
-
const ir = compileModel(`
|
|
119
|
-
export interface Order {
|
|
120
|
-
total: number;
|
|
121
|
-
status: 'a' | 'b';
|
|
122
|
-
}
|
|
123
|
-
export const transitions = {};
|
|
124
|
-
export const fieldOptions = {
|
|
125
|
-
total: { computed: 'fields.quantity * fields.unit_price', computed_deps: ['quantity', 'unit_price'] },
|
|
126
|
-
};
|
|
127
|
-
`);
|
|
128
|
-
|
|
129
|
-
const total = ir.fields.find((f) => f.name === 'total');
|
|
130
|
-
expect(total!.computed).toBe('fields.quantity * fields.unit_price');
|
|
131
|
-
expect(total!.computed_deps).toEqual(['quantity', 'unit_price']);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it('should apply state home', () => {
|
|
135
|
-
const ir = compileModel(`
|
|
136
|
-
export interface Chat {
|
|
137
|
-
draft: string;
|
|
138
|
-
status: 'a' | 'b';
|
|
139
|
-
}
|
|
140
|
-
export const transitions = {};
|
|
141
|
-
export const fieldOptions = {
|
|
142
|
-
draft: { scope: 'route', persistence: 'local', sync: 'none' },
|
|
143
|
-
};
|
|
144
|
-
`);
|
|
145
|
-
|
|
146
|
-
const draft = ir.fields.find((f) => f.name === 'draft');
|
|
147
|
-
expect(draft!.state_home).toEqual({
|
|
148
|
-
scope: 'route',
|
|
149
|
-
persistence: 'local',
|
|
150
|
-
sync: 'none',
|
|
151
|
-
});
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it('should apply validation rules', () => {
|
|
155
|
-
const ir = compileModel(`
|
|
156
|
-
export interface Form {
|
|
157
|
-
email: string;
|
|
158
|
-
status: 'a' | 'b';
|
|
159
|
-
}
|
|
160
|
-
export const transitions = {};
|
|
161
|
-
export const fieldOptions = {
|
|
162
|
-
email: {
|
|
163
|
-
validation: {
|
|
164
|
-
minLength: 5,
|
|
165
|
-
maxLength: 255,
|
|
166
|
-
rules: [{ expression: 'contains(email, "@")', message: 'Must be valid email', severity: 'error' }],
|
|
167
|
-
},
|
|
168
|
-
},
|
|
169
|
-
};
|
|
170
|
-
`);
|
|
171
|
-
|
|
172
|
-
const email = ir.fields.find((f) => f.name === 'email');
|
|
173
|
-
expect(email!.validation?.minLength).toBe(5);
|
|
174
|
-
expect(email!.validation?.maxLength).toBe(255);
|
|
175
|
-
expect(email!.validation?.rules).toHaveLength(1);
|
|
176
|
-
expect(email!.validation?.rules![0].expression).toBe('contains(email, "@")');
|
|
177
|
-
expect(email!.validation?.rules![0].message).toBe('Must be valid email');
|
|
178
|
-
expect(email!.validation?.rules![0].severity).toBe('error');
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it('should merge validation with existing options', () => {
|
|
182
|
-
const ir = compileModel(`
|
|
183
|
-
export interface Selector {
|
|
184
|
-
priority: 'low' | 'medium' | 'high';
|
|
185
|
-
status: 'a' | 'b';
|
|
186
|
-
}
|
|
187
|
-
export const transitions = {};
|
|
188
|
-
export const fieldOptions = {
|
|
189
|
-
priority: {
|
|
190
|
-
validation: { min: 0, max: 10 },
|
|
191
|
-
},
|
|
192
|
-
};
|
|
193
|
-
`);
|
|
194
|
-
|
|
195
|
-
const priority = ir.fields.find((f) => f.name === 'priority');
|
|
196
|
-
// Should keep existing options from union type AND add min/max
|
|
197
|
-
expect(priority!.validation?.options).toEqual(['low', 'medium', 'high']);
|
|
198
|
-
expect(priority!.validation?.min).toBe(0);
|
|
199
|
-
expect(priority!.validation?.max).toBe(10);
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it('should combine multiple field options', () => {
|
|
203
|
-
const ir = compileModel(`
|
|
204
|
-
export interface Full {
|
|
205
|
-
data: string;
|
|
206
|
-
status: 'a' | 'b';
|
|
207
|
-
}
|
|
208
|
-
export const transitions = {};
|
|
209
|
-
export const fieldOptions = {
|
|
210
|
-
data: {
|
|
211
|
-
scope: 'instance',
|
|
212
|
-
persistence: 'durable',
|
|
213
|
-
sync: 'tenant',
|
|
214
|
-
computed: 'upper(data)',
|
|
215
|
-
computed_deps: ['data'],
|
|
216
|
-
visible_in_states: ['a'],
|
|
217
|
-
editable_in_states: ['a'],
|
|
218
|
-
visible_to_roles: ['admin'],
|
|
219
|
-
editable_by_roles: ['admin'],
|
|
220
|
-
visible_when: '$state == "a"',
|
|
221
|
-
editable_when: '$user.role == "admin"',
|
|
222
|
-
},
|
|
223
|
-
};
|
|
224
|
-
`);
|
|
225
|
-
|
|
226
|
-
const data = ir.fields.find((f) => f.name === 'data');
|
|
227
|
-
expect(data!.state_home).toBeDefined();
|
|
228
|
-
expect(data!.computed).toBe('upper(data)');
|
|
229
|
-
expect(data!.computed_deps).toEqual(['data']);
|
|
230
|
-
expect(data!.visible_in_states).toEqual(['a']);
|
|
231
|
-
expect(data!.editable_in_states).toEqual(['a']);
|
|
232
|
-
expect(data!.visible_to_roles).toEqual(['admin']);
|
|
233
|
-
expect(data!.editable_by_roles).toEqual(['admin']);
|
|
234
|
-
expect(data!.visible_when).toBe('$state == "a"');
|
|
235
|
-
expect(data!.editable_when).toBe('$user.role == "admin"');
|
|
236
|
-
});
|
|
237
|
-
});
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Model Hooks Tests — validates hook extraction (on_enter/on_exit actions, emit events, server actions).
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { describe, it, expect } from 'vitest';
|
|
6
|
-
import { transformSync } from '@babel/core';
|
|
7
|
-
import babelPlugin from '../babel';
|
|
8
|
-
import type { IRWorkflowDefinition } from '@mindmatrix/player-core';
|
|
9
|
-
|
|
10
|
-
function compileModel(code: string): IRWorkflowDefinition {
|
|
11
|
-
const result = transformSync(code, {
|
|
12
|
-
filename: 'models/test.ts',
|
|
13
|
-
plugins: [[babelPlugin, { mode: 'strict' }]],
|
|
14
|
-
parserOpts: { plugins: ['typescript'], attachComment: true },
|
|
15
|
-
});
|
|
16
|
-
return (result as any)?.metadata?.mindmatrixIR;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
describe('Model Hooks', () => {
|
|
20
|
-
it('should extract on_enter hooks with emit event action', () => {
|
|
21
|
-
const ir = compileModel(`
|
|
22
|
-
export interface Item {
|
|
23
|
-
status: 'draft' | 'active';
|
|
24
|
-
}
|
|
25
|
-
export const transitions = {
|
|
26
|
-
activate: { from: 'draft', to: 'active' },
|
|
27
|
-
};
|
|
28
|
-
export const hooks = {
|
|
29
|
-
on_enter_active: { emit: 'item:activated' },
|
|
30
|
-
};
|
|
31
|
-
`);
|
|
32
|
-
|
|
33
|
-
const activeState = ir.states.find((s) => s.name === 'active');
|
|
34
|
-
expect(activeState).toBeDefined();
|
|
35
|
-
expect(activeState!.on_enter).toHaveLength(1);
|
|
36
|
-
expect(activeState!.on_enter[0].type).toBe('emit_event');
|
|
37
|
-
expect(activeState!.on_enter[0].config.event).toBe('item:activated');
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('should extract on_enter hooks with server action references', () => {
|
|
41
|
-
const ir = compileModel(`
|
|
42
|
-
export interface Channel {
|
|
43
|
-
status: 'draft' | 'active';
|
|
44
|
-
}
|
|
45
|
-
export const transitions = {};
|
|
46
|
-
export const hooks = {
|
|
47
|
-
on_enter_active: { actions: ['notifyMembers', 'indexForSearch'] },
|
|
48
|
-
};
|
|
49
|
-
`);
|
|
50
|
-
|
|
51
|
-
const activeState = ir.states.find((s) => s.name === 'active');
|
|
52
|
-
expect(activeState!.on_enter).toHaveLength(2);
|
|
53
|
-
expect(activeState!.on_enter[0].type).toBe('server_action');
|
|
54
|
-
expect(activeState!.on_enter[0].config.handler).toBe('notifyMembers');
|
|
55
|
-
expect(activeState!.on_enter[1].type).toBe('server_action');
|
|
56
|
-
expect(activeState!.on_enter[1].config.handler).toBe('indexForSearch');
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('should extract on_exit hooks', () => {
|
|
60
|
-
const ir = compileModel(`
|
|
61
|
-
export interface Session {
|
|
62
|
-
status: 'active' | 'ended';
|
|
63
|
-
}
|
|
64
|
-
export const transitions = {};
|
|
65
|
-
export const hooks = {
|
|
66
|
-
on_exit_active: { actions: ['cleanup'] },
|
|
67
|
-
};
|
|
68
|
-
`);
|
|
69
|
-
|
|
70
|
-
const activeState = ir.states.find((s) => s.name === 'active');
|
|
71
|
-
expect(activeState!.on_exit).toHaveLength(1);
|
|
72
|
-
expect(activeState!.on_exit[0].type).toBe('server_action');
|
|
73
|
-
expect(activeState!.on_exit[0].config.handler).toBe('cleanup');
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('should combine emit and actions in same hook', () => {
|
|
77
|
-
const ir = compileModel(`
|
|
78
|
-
export interface Flow {
|
|
79
|
-
status: 'pending' | 'approved';
|
|
80
|
-
}
|
|
81
|
-
export const transitions = {};
|
|
82
|
-
export const hooks = {
|
|
83
|
-
on_enter_approved: { emit: 'flow:approved', actions: ['notifyOwner', 'logApproval'] },
|
|
84
|
-
};
|
|
85
|
-
`);
|
|
86
|
-
|
|
87
|
-
const approvedState = ir.states.find((s) => s.name === 'approved');
|
|
88
|
-
expect(approvedState!.on_enter).toHaveLength(3); // 1 emit + 2 server actions
|
|
89
|
-
expect(approvedState!.on_enter[0].type).toBe('emit_event');
|
|
90
|
-
expect(approvedState!.on_enter[1].type).toBe('server_action');
|
|
91
|
-
expect(approvedState!.on_enter[2].type).toBe('server_action');
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it('should handle hooks for states not yet declared', () => {
|
|
95
|
-
const ir = compileModel(`
|
|
96
|
-
export interface Widget {
|
|
97
|
-
status: 'a' | 'b';
|
|
98
|
-
}
|
|
99
|
-
export const transitions = {};
|
|
100
|
-
export const hooks = {
|
|
101
|
-
on_enter_c: { emit: 'widget:c_entered' },
|
|
102
|
-
};
|
|
103
|
-
`);
|
|
104
|
-
|
|
105
|
-
const cState = ir.states.find((s) => s.name === 'c');
|
|
106
|
-
expect(cState).toBeDefined();
|
|
107
|
-
expect(cState!.on_enter).toHaveLength(1);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('should have unique action IDs across hooks', () => {
|
|
111
|
-
const ir = compileModel(`
|
|
112
|
-
export interface Multi {
|
|
113
|
-
status: 'a' | 'b' | 'c';
|
|
114
|
-
}
|
|
115
|
-
export const transitions = {};
|
|
116
|
-
export const hooks = {
|
|
117
|
-
on_enter_a: { actions: ['h1'] },
|
|
118
|
-
on_enter_b: { actions: ['h2'] },
|
|
119
|
-
on_exit_a: { actions: ['h3'] },
|
|
120
|
-
};
|
|
121
|
-
`);
|
|
122
|
-
|
|
123
|
-
const allActionIds: string[] = [];
|
|
124
|
-
for (const state of ir.states) {
|
|
125
|
-
for (const action of state.on_enter) allActionIds.push(action.id);
|
|
126
|
-
for (const action of state.on_exit) allActionIds.push(action.id);
|
|
127
|
-
}
|
|
128
|
-
expect(new Set(allActionIds).size).toBe(allActionIds.length);
|
|
129
|
-
});
|
|
130
|
-
});
|
|
@@ -1,268 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Model Ref Resolution Tests — validates useQuery(modelRef) and useMutation(modelRef)
|
|
3
|
-
* where modelRef is an imported defineModel result.
|
|
4
|
-
*
|
|
5
|
-
* The compiler should resolve the import path to derive the slug, allowing:
|
|
6
|
-
* import ChannelModel from '../models/channel';
|
|
7
|
-
* const { data } = useQuery(ChannelModel);
|
|
8
|
-
* instead of requiring:
|
|
9
|
-
* const { data } = useQuery('channel');
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { describe, it, expect } from 'vitest';
|
|
13
|
-
import { transformSync } from '@babel/core';
|
|
14
|
-
import babelPlugin from '../babel';
|
|
15
|
-
import type { IRWorkflowDefinition } from '@mindmatrix/player-core';
|
|
16
|
-
|
|
17
|
-
function compile(code: string, filename = 'test.workflow.tsx'): IRWorkflowDefinition {
|
|
18
|
-
const result = transformSync(code, {
|
|
19
|
-
filename,
|
|
20
|
-
plugins: [[babelPlugin]],
|
|
21
|
-
parserOpts: { plugins: ['typescript', 'jsx'], attachComment: true },
|
|
22
|
-
});
|
|
23
|
-
return (result as any)?.metadata?.mindmatrixIR;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
describe('useQuery — model ref resolution', () => {
|
|
27
|
-
it('should resolve slug from default model import', () => {
|
|
28
|
-
const ir = compile(`
|
|
29
|
-
import ChannelModel from '../models/channel';
|
|
30
|
-
export function Dashboard() {
|
|
31
|
-
const { data: channels } = useQuery(ChannelModel);
|
|
32
|
-
return <div />;
|
|
33
|
-
}
|
|
34
|
-
`);
|
|
35
|
-
|
|
36
|
-
const sources = (ir.metadata as any).dataSources;
|
|
37
|
-
expect(sources).toHaveLength(1);
|
|
38
|
-
expect(sources[0].slug).toBe('channel');
|
|
39
|
-
expect(sources[0].type).toBe('workflow');
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('should resolve slug from PascalCase model filename', () => {
|
|
43
|
-
const ir = compile(`
|
|
44
|
-
import UserProfile from '../models/UserProfile';
|
|
45
|
-
export function Dashboard() {
|
|
46
|
-
const { data } = useQuery(UserProfile);
|
|
47
|
-
return <div />;
|
|
48
|
-
}
|
|
49
|
-
`);
|
|
50
|
-
|
|
51
|
-
const sources = (ir.metadata as any).dataSources;
|
|
52
|
-
expect(sources).toHaveLength(1);
|
|
53
|
-
expect(sources[0].slug).toBe('user-profile');
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('should resolve slug from kebab-case model filename', () => {
|
|
57
|
-
const ir = compile(`
|
|
58
|
-
import OrderItem from '../models/order-item';
|
|
59
|
-
export function Dashboard() {
|
|
60
|
-
const { data } = useQuery(OrderItem);
|
|
61
|
-
return <div />;
|
|
62
|
-
}
|
|
63
|
-
`);
|
|
64
|
-
|
|
65
|
-
const sources = (ir.metadata as any).dataSources;
|
|
66
|
-
expect(sources).toHaveLength(1);
|
|
67
|
-
expect(sources[0].slug).toBe('order-item');
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('should resolve slug stripping file extension', () => {
|
|
71
|
-
const ir = compile(`
|
|
72
|
-
import ProductModel from '../models/product.ts';
|
|
73
|
-
export function Dashboard() {
|
|
74
|
-
const { data } = useQuery(ProductModel);
|
|
75
|
-
return <div />;
|
|
76
|
-
}
|
|
77
|
-
`);
|
|
78
|
-
|
|
79
|
-
const sources = (ir.metadata as any).dataSources;
|
|
80
|
-
expect(sources).toHaveLength(1);
|
|
81
|
-
expect(sources[0].slug).toBe('product');
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('should still work with string literal slugs', () => {
|
|
85
|
-
const ir = compile(`
|
|
86
|
-
import { useQuery } from '@mindmatrix/react';
|
|
87
|
-
export function Dashboard() {
|
|
88
|
-
const { data } = useQuery('channel');
|
|
89
|
-
return <div />;
|
|
90
|
-
}
|
|
91
|
-
`);
|
|
92
|
-
|
|
93
|
-
const sources = (ir.metadata as any).dataSources;
|
|
94
|
-
expect(sources).toHaveLength(1);
|
|
95
|
-
expect(sources[0].slug).toBe('channel');
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('should resolve model ref with query options', () => {
|
|
99
|
-
const ir = compile(`
|
|
100
|
-
import ChannelModel from '../models/channel';
|
|
101
|
-
export function Dashboard() {
|
|
102
|
-
const { data } = useQuery(ChannelModel, { limit: 25, orderBy: 'name' });
|
|
103
|
-
return <div />;
|
|
104
|
-
}
|
|
105
|
-
`);
|
|
106
|
-
|
|
107
|
-
const sources = (ir.metadata as any).dataSources;
|
|
108
|
-
expect(sources).toHaveLength(1);
|
|
109
|
-
expect(sources[0].slug).toBe('channel');
|
|
110
|
-
expect(sources[0].pageSize).toBe(25);
|
|
111
|
-
expect(sources[0].sort).toBe('name');
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('should resolve named import from model file', () => {
|
|
115
|
-
const ir = compile(`
|
|
116
|
-
import { Channel } from '../models/channel';
|
|
117
|
-
export function Dashboard() {
|
|
118
|
-
const { data } = useQuery(Channel);
|
|
119
|
-
return <div />;
|
|
120
|
-
}
|
|
121
|
-
`);
|
|
122
|
-
|
|
123
|
-
const sources = (ir.metadata as any).dataSources;
|
|
124
|
-
expect(sources).toHaveLength(1);
|
|
125
|
-
expect(sources[0].slug).toBe('channel');
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it('should handle multiple model imports', () => {
|
|
129
|
-
const ir = compile(`
|
|
130
|
-
import ChannelModel from '../models/channel';
|
|
131
|
-
import MessageModel from '../models/message';
|
|
132
|
-
export function Dashboard() {
|
|
133
|
-
const { data: channels } = useQuery(ChannelModel);
|
|
134
|
-
const { data: messages } = useQuery(MessageModel);
|
|
135
|
-
return <div />;
|
|
136
|
-
}
|
|
137
|
-
`);
|
|
138
|
-
|
|
139
|
-
const sources = (ir.metadata as any).dataSources;
|
|
140
|
-
expect(sources).toHaveLength(2);
|
|
141
|
-
expect(sources[0].slug).toBe('channel');
|
|
142
|
-
expect(sources[1].slug).toBe('message');
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it('should ignore non-model imports as useQuery arguments', () => {
|
|
146
|
-
const ir = compile(`
|
|
147
|
-
import { someUtil } from '../utils/helpers';
|
|
148
|
-
export function Dashboard() {
|
|
149
|
-
const { data } = useQuery('channel');
|
|
150
|
-
return <div />;
|
|
151
|
-
}
|
|
152
|
-
`);
|
|
153
|
-
|
|
154
|
-
const sources = (ir.metadata as any).dataSources;
|
|
155
|
-
expect(sources).toHaveLength(1);
|
|
156
|
-
expect(sources[0].slug).toBe('channel');
|
|
157
|
-
});
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
describe('useMutation — model ref resolution', () => {
|
|
161
|
-
it('should resolve slug from default model import', () => {
|
|
162
|
-
const ir = compile(`
|
|
163
|
-
import ChannelModel from '../models/channel';
|
|
164
|
-
export function ChannelForm() {
|
|
165
|
-
const mutation = useMutation(ChannelModel);
|
|
166
|
-
return <div />;
|
|
167
|
-
}
|
|
168
|
-
`);
|
|
169
|
-
|
|
170
|
-
const targets = (ir.metadata as any).mutationTargets;
|
|
171
|
-
expect(targets).toHaveLength(1);
|
|
172
|
-
expect(targets[0]).toBe('channel');
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it('should resolve slug from PascalCase model filename for mutation', () => {
|
|
176
|
-
const ir = compile(`
|
|
177
|
-
import UserProfile from '../models/UserProfile';
|
|
178
|
-
export function ProfileEditor() {
|
|
179
|
-
const mutation = useMutation(UserProfile);
|
|
180
|
-
return <div />;
|
|
181
|
-
}
|
|
182
|
-
`);
|
|
183
|
-
|
|
184
|
-
const targets = (ir.metadata as any).mutationTargets;
|
|
185
|
-
expect(targets).toHaveLength(1);
|
|
186
|
-
expect(targets[0]).toBe('user-profile');
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
it('should still work with string literal slugs for mutation', () => {
|
|
190
|
-
const ir = compile(`
|
|
191
|
-
import { useMutation } from '@mindmatrix/react';
|
|
192
|
-
export function ChannelForm() {
|
|
193
|
-
const mutation = useMutation('channel');
|
|
194
|
-
return <div />;
|
|
195
|
-
}
|
|
196
|
-
`);
|
|
197
|
-
|
|
198
|
-
const targets = (ir.metadata as any).mutationTargets;
|
|
199
|
-
expect(targets).toHaveLength(1);
|
|
200
|
-
expect(targets[0]).toBe('channel');
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
it('should handle mixed model ref and string literal', () => {
|
|
204
|
-
const ir = compile(`
|
|
205
|
-
import ChannelModel from '../models/channel';
|
|
206
|
-
export function Dashboard() {
|
|
207
|
-
const { data } = useQuery(ChannelModel);
|
|
208
|
-
const mutation = useMutation('message');
|
|
209
|
-
return <div />;
|
|
210
|
-
}
|
|
211
|
-
`);
|
|
212
|
-
|
|
213
|
-
const sources = (ir.metadata as any).dataSources;
|
|
214
|
-
const targets = (ir.metadata as any).mutationTargets;
|
|
215
|
-
expect(sources).toHaveLength(1);
|
|
216
|
-
expect(sources[0].slug).toBe('channel');
|
|
217
|
-
expect(targets).toHaveLength(1);
|
|
218
|
-
expect(targets[0]).toBe('message');
|
|
219
|
-
});
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
describe('Model import tracking', () => {
|
|
223
|
-
it('should only track imports from model paths', () => {
|
|
224
|
-
const ir = compile(`
|
|
225
|
-
import React from 'react';
|
|
226
|
-
import { useQuery } from '@mindmatrix/react';
|
|
227
|
-
import ChannelModel from '../models/channel';
|
|
228
|
-
import { formatDate } from '../utils/format';
|
|
229
|
-
export function Dashboard() {
|
|
230
|
-
const { data } = useQuery(ChannelModel);
|
|
231
|
-
return <div />;
|
|
232
|
-
}
|
|
233
|
-
`);
|
|
234
|
-
|
|
235
|
-
// Should resolve the model import
|
|
236
|
-
const sources = (ir.metadata as any).dataSources;
|
|
237
|
-
expect(sources).toHaveLength(1);
|
|
238
|
-
expect(sources[0].slug).toBe('channel');
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
it('should not leak __modelImports into emitted metadata', () => {
|
|
242
|
-
const ir = compile(`
|
|
243
|
-
import ChannelModel from '../models/channel';
|
|
244
|
-
export function Dashboard() {
|
|
245
|
-
const { data } = useQuery(ChannelModel);
|
|
246
|
-
return <div />;
|
|
247
|
-
}
|
|
248
|
-
`);
|
|
249
|
-
|
|
250
|
-
// Internal tracking keys should be stripped
|
|
251
|
-
expect((ir.metadata as any).__modelImports).toBeUndefined();
|
|
252
|
-
expect((ir.metadata as any).__modelImportSlugs).toBeUndefined();
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
it('should track .model suffix imports', () => {
|
|
256
|
-
const ir = compile(`
|
|
257
|
-
import TaskModel from '../task.model';
|
|
258
|
-
export function Dashboard() {
|
|
259
|
-
const { data } = useQuery(TaskModel);
|
|
260
|
-
return <div />;
|
|
261
|
-
}
|
|
262
|
-
`);
|
|
263
|
-
|
|
264
|
-
const sources = (ir.metadata as any).dataSources;
|
|
265
|
-
expect(sources).toHaveLength(1);
|
|
266
|
-
expect(sources[0].slug).toBe('task');
|
|
267
|
-
});
|
|
268
|
-
});
|