@playcanvas/attribute-parser 1.5.1 → 1.6.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/.github/workflows/ci.yml +2 -2
- package/README.md +42 -46
- package/package.json +1 -1
- package/src/index.js +17 -0
- package/src/parsers/attribute-parser.js +1 -4
- package/src/parsers/script-parser.js +8 -2
- package/src/utils/ts-utils.js +18 -0
- package/test/fixtures/json.valid.js +10 -1
- package/test/fixtures/json.valid.ts +10 -1
- package/test/fixtures/misc.valid.js +3 -1
- package/test/fixtures/program.valid.js +2 -0
- package/test/tests/invalid/asset.test.js +3 -3
- package/test/tests/valid/asset.test.js +2 -2
- package/test/tests/valid/json.test.js +38 -0
- package/test/tests/valid/misc.test.js +1 -1
- package/test/tests/valid/program.test.js +6 -1
package/.github/workflows/ci.yml
CHANGED
|
@@ -18,7 +18,7 @@ jobs:
|
|
|
18
18
|
- name: Setup Node.js 18.x
|
|
19
19
|
uses: actions/setup-node@v4
|
|
20
20
|
with:
|
|
21
|
-
node-version:
|
|
21
|
+
node-version: 22.x
|
|
22
22
|
cache: 'npm'
|
|
23
23
|
|
|
24
24
|
- name: Install dependencies
|
|
@@ -39,7 +39,7 @@ jobs:
|
|
|
39
39
|
- name: Setup Node.js 18.x
|
|
40
40
|
uses: actions/setup-node@v4
|
|
41
41
|
with:
|
|
42
|
-
node-version:
|
|
42
|
+
node-version: 22.x
|
|
43
43
|
cache: 'npm'
|
|
44
44
|
|
|
45
45
|
- name: Install dependencies
|
package/README.md
CHANGED
|
@@ -1,59 +1,55 @@
|
|
|
1
1
|
# PlayCanvas Attribute Parser
|
|
2
2
|
|
|
3
|
-
This the official JSDoc attribute parser used in the PlayCanvas
|
|
3
|
+
This is the official JSDoc attribute parser used in the PlayCanvas Editor.
|
|
4
4
|
|
|
5
|
-
It
|
|
5
|
+
It collects metadata from user scripts by parsing `@attribute` JSDoc annotations. These attributes enable the PlayCanvas Editor to expose UI controls and contextual information for your script properties.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Example
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Given a script like this:
|
|
10
10
|
|
|
11
11
|
```javascript
|
|
12
12
|
class Rotator extends ScriptType {
|
|
13
13
|
/**
|
|
14
14
|
* @attribute
|
|
15
|
-
* Speed determines how fast to rotate things
|
|
15
|
+
* Speed determines how fast to rotate things
|
|
16
16
|
*/
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
17
|
+
speed = new Vec3();
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @attribute
|
|
21
|
+
* An array of Entities to rotate
|
|
22
|
+
*
|
|
23
|
+
* @type {Entity[]}
|
|
24
|
+
*/
|
|
25
|
+
thingsToRotate;
|
|
26
26
|
}
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
The parser outputs:
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
```json
|
|
33
33
|
{
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
},
|
|
55
|
-
"errors": []
|
|
56
|
-
}
|
|
34
|
+
"rotator": {
|
|
35
|
+
"attributes": {
|
|
36
|
+
"speed": {
|
|
37
|
+
"type": "vec3",
|
|
38
|
+
"name": "speed",
|
|
39
|
+
"array": false,
|
|
40
|
+
"description": "Speed determines how fast to rotate things",
|
|
41
|
+
"default": [0, 0, 0]
|
|
42
|
+
},
|
|
43
|
+
"thingsToRotate": {
|
|
44
|
+
"type": "entity",
|
|
45
|
+
"name": "thingsToRotate",
|
|
46
|
+
"array": true,
|
|
47
|
+
"description": "An array of Entities to rotate",
|
|
48
|
+
"default": null
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"errors": []
|
|
52
|
+
}
|
|
57
53
|
}
|
|
58
54
|
```
|
|
59
55
|
|
|
@@ -63,26 +59,26 @@ JSDocs tags are parsed and values and outputs the metadata in a serializable for
|
|
|
63
59
|
|
|
64
60
|
```javascript
|
|
65
61
|
// Initialise the parser
|
|
62
|
+
// Initialize the parser
|
|
66
63
|
const parser = new JSDocParser();
|
|
67
64
|
await parser.init();
|
|
68
65
|
|
|
69
|
-
//
|
|
66
|
+
// Load your source files: {[filename: string]: string}[]
|
|
70
67
|
const scripts = await fetchScripts([...paths, './playcanvas.d.ts']);
|
|
71
68
|
|
|
72
|
-
//
|
|
73
|
-
parser.updateProgram(scripts)
|
|
69
|
+
// Update the parser program
|
|
70
|
+
parser.updateProgram(scripts);
|
|
74
71
|
|
|
75
|
-
// Parse
|
|
76
|
-
const [
|
|
72
|
+
// Parse attributes from the entry point
|
|
73
|
+
const [attributes, errors] = parser.parseAttributes("./index.js");
|
|
77
74
|
```
|
|
78
75
|
|
|
79
76
|
### Attribute structure
|
|
80
77
|
|
|
81
|
-
|
|
82
|
-
|
|
78
|
+
See the [test fixtures](https://github.com/playcanvas/attribute-parser/tree/main/test/fixtures) for examples of supported JSDoc tags and output formats.
|
|
83
79
|
|
|
84
80
|
### Tests
|
|
85
81
|
|
|
86
|
-
|
|
82
|
+
This project includes good test coverage for all supported tag formats and edge cases.
|
|
87
83
|
|
|
88
84
|
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -143,9 +143,26 @@ export class JSDocParser {
|
|
|
143
143
|
|
|
144
144
|
const esmScriptClass = pcTypes.statements.find(node => node.kind === ts.SyntaxKind.ClassDeclaration && node.name.text === 'Script')?.symbol;
|
|
145
145
|
|
|
146
|
+
const isScriptNameMember = member => (
|
|
147
|
+
ts.isPropertyDeclaration(member) && // Is a property declaration
|
|
148
|
+
ts.isIdentifier(member.name) &&
|
|
149
|
+
member.name.text === 'scriptName' &&
|
|
150
|
+
(ts.getCombinedModifierFlags(member) & ts.ModifierFlags.Static) !== 0 &&
|
|
151
|
+
ts.isStringLiteral(member.initializer)
|
|
152
|
+
);
|
|
153
|
+
|
|
146
154
|
// Check if the file exports a class that inherits from `Script`
|
|
147
155
|
nodes.forEach((node, name) => {
|
|
148
156
|
if (isAliasedClassDeclaration(node, typeChecker) && inheritsFrom(node, typeChecker, esmScriptClass)) {
|
|
157
|
+
|
|
158
|
+
// Check for a static scriptName property and use that as the name if it exists
|
|
159
|
+
const scriptNameMember = node.members.find(isScriptNameMember);
|
|
160
|
+
|
|
161
|
+
// If the scriptName property exists, use that as the name
|
|
162
|
+
if (scriptNameMember) {
|
|
163
|
+
name = scriptNameMember.initializer.text;
|
|
164
|
+
}
|
|
165
|
+
|
|
149
166
|
esmScripts.set(name, node);
|
|
150
167
|
}
|
|
151
168
|
});
|
|
@@ -46,10 +46,7 @@ export class AttributeParser {
|
|
|
46
46
|
if (!attribute) return;
|
|
47
47
|
|
|
48
48
|
// Extract the description from the summary section
|
|
49
|
-
|
|
50
|
-
if (description) {
|
|
51
|
-
attribute.description = description.split('.')[0];
|
|
52
|
-
}
|
|
49
|
+
attribute.description = node.jsDoc?.[0]?.comment || '';
|
|
53
50
|
|
|
54
51
|
// Extract metadata from custom tags
|
|
55
52
|
const jsDocTags = node.jsDoc?.[0]?.tags ?? [];
|
|
@@ -186,6 +186,12 @@ const mapAttributesToOutput = (attribute) => {
|
|
|
186
186
|
attribute.type = 'rgba';
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
// 'resource' type needs to be mapped to 'assetType'
|
|
190
|
+
if (attribute.resource) {
|
|
191
|
+
attribute.assetType = attribute.resource;
|
|
192
|
+
delete attribute.resource;
|
|
193
|
+
}
|
|
194
|
+
|
|
189
195
|
// remove enum if it's empty
|
|
190
196
|
if (attribute.enum.length === 0) delete attribute.enum;
|
|
191
197
|
|
|
@@ -388,9 +394,9 @@ export class ScriptParser {
|
|
|
388
394
|
Array.from(type.symbol.members).map(arr => arr[1].declarations[0]) ?? // a typedefs property declarations
|
|
389
395
|
[];
|
|
390
396
|
|
|
391
|
-
// If the type is an interface, we need to parse the attribute comments, if not, just the intitialized object
|
|
397
|
+
// If the type is an interface or an initialized object, we need to parse the attribute comments, if not, just the intitialized object
|
|
392
398
|
let commentAttributes;
|
|
393
|
-
if (typeIsInterface) {
|
|
399
|
+
if (typeIsInterface || isInitialized) {
|
|
394
400
|
commentAttributes = this.extractAttributes(typeNode, { errors, requiresAttributeTag: false, depth: depth + 1 });
|
|
395
401
|
}
|
|
396
402
|
|
package/src/utils/ts-utils.js
CHANGED
|
@@ -209,6 +209,7 @@ function getSuperClasses(node, typeChecker) {
|
|
|
209
209
|
export function getJSDocTags(node, typeChecker) {
|
|
210
210
|
const tags = [];
|
|
211
211
|
|
|
212
|
+
// check if class interface of initialized object
|
|
212
213
|
if (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node)) {
|
|
213
214
|
// get an array of the class an all parent classed
|
|
214
215
|
const heritageChain = getSuperClasses(node, typeChecker)
|
|
@@ -240,6 +241,23 @@ export function getJSDocTags(node, typeChecker) {
|
|
|
240
241
|
}
|
|
241
242
|
});
|
|
242
243
|
});
|
|
244
|
+
} else if (ts.isObjectLiteralExpression(node)) {
|
|
245
|
+
// Handle object literals
|
|
246
|
+
node.properties.forEach((property) => {
|
|
247
|
+
if (ts.isPropertyAssignment(property)) {
|
|
248
|
+
const memberName = property.name &&
|
|
249
|
+
(ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) ?
|
|
250
|
+
property.name.text :
|
|
251
|
+
'unnamed';
|
|
252
|
+
|
|
253
|
+
if (property.jsDoc?.length > 0) {
|
|
254
|
+
tags.push({
|
|
255
|
+
memberName,
|
|
256
|
+
member: property
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
});
|
|
243
261
|
}
|
|
244
262
|
|
|
245
263
|
return tags;
|
|
@@ -6,6 +6,7 @@ import { Script, Vec3 } from 'playcanvas';
|
|
|
6
6
|
// eslint-disable-next-line
|
|
7
7
|
class Folder {
|
|
8
8
|
/**
|
|
9
|
+
* This is some description of the attribute.
|
|
9
10
|
* @attribute
|
|
10
11
|
* @type {boolean}
|
|
11
12
|
*/
|
|
@@ -13,6 +14,7 @@ class Folder {
|
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* @attribute
|
|
17
|
+
* @range [10, 20]
|
|
16
18
|
*/
|
|
17
19
|
b = 10;
|
|
18
20
|
|
|
@@ -64,7 +66,14 @@ class Example extends Script {
|
|
|
64
66
|
/**
|
|
65
67
|
* @attribute
|
|
66
68
|
*/
|
|
67
|
-
i = {
|
|
69
|
+
i = {
|
|
70
|
+
/** description of a */
|
|
71
|
+
a: true,
|
|
72
|
+
/** @range [10, 20] */
|
|
73
|
+
b: 10,
|
|
74
|
+
c: 'hello',
|
|
75
|
+
d: [new Vec3(1, 2, 3)]
|
|
76
|
+
};
|
|
68
77
|
|
|
69
78
|
/**
|
|
70
79
|
* @typedef {object} TypeDefFolder
|
|
@@ -2,6 +2,7 @@ import { Script, Vec3 } from 'playcanvas';
|
|
|
2
2
|
|
|
3
3
|
interface Folder {
|
|
4
4
|
/**
|
|
5
|
+
* This is some description of the attribute.
|
|
5
6
|
* @attribute
|
|
6
7
|
*/
|
|
7
8
|
a: boolean;
|
|
@@ -9,6 +10,7 @@ interface Folder {
|
|
|
9
10
|
/**
|
|
10
11
|
* @attribute
|
|
11
12
|
* @default 10
|
|
13
|
+
* @range [10, 20]
|
|
12
14
|
*/
|
|
13
15
|
b: number;
|
|
14
16
|
|
|
@@ -54,7 +56,14 @@ class Example extends Script {
|
|
|
54
56
|
/**
|
|
55
57
|
* @attribute
|
|
56
58
|
*/
|
|
57
|
-
i = {
|
|
59
|
+
i = {
|
|
60
|
+
/** description of a */
|
|
61
|
+
a: true,
|
|
62
|
+
/** @range [10, 20] */
|
|
63
|
+
b: 10,
|
|
64
|
+
c: 'hello',
|
|
65
|
+
d: [new Vec3(1, 2, 3)]
|
|
66
|
+
};
|
|
58
67
|
|
|
59
68
|
/**
|
|
60
69
|
* @attribute
|
|
@@ -26,7 +26,7 @@ describe('INVALID: Asset attribute', function () {
|
|
|
26
26
|
expect(data[0].example.attributes.a.name).to.equal('a');
|
|
27
27
|
expect(data[0].example.attributes.a.type).to.equal('asset');
|
|
28
28
|
expect(data[0].example.attributes.a.default).to.not.exist;
|
|
29
|
-
expect(data[0].example.attributes.a.
|
|
29
|
+
expect(data[0].example.attributes.a.assetType).to.not.exist;
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
it('b: should fallback to an asset attribute without resource', function () {
|
|
@@ -34,7 +34,7 @@ describe('INVALID: Asset attribute', function () {
|
|
|
34
34
|
expect(data[0].example.attributes.b.name).to.equal('b');
|
|
35
35
|
expect(data[0].example.attributes.b.type).to.equal('asset');
|
|
36
36
|
expect(data[0].example.attributes.b.default).to.not.exist;
|
|
37
|
-
expect(data[0].example.attributes.b.
|
|
37
|
+
expect(data[0].example.attributes.b.assetType).to.not.exist;
|
|
38
38
|
});
|
|
39
39
|
|
|
40
40
|
it('c: should fallback to an asset attribute without resource', function () {
|
|
@@ -42,6 +42,6 @@ describe('INVALID: Asset attribute', function () {
|
|
|
42
42
|
expect(data[0].example.attributes.c.name).to.equal('c');
|
|
43
43
|
expect(data[0].example.attributes.c.type).to.equal('asset');
|
|
44
44
|
expect(data[0].example.attributes.c.default).to.not.exist;
|
|
45
|
-
expect(data[0].example.attributes.c.
|
|
45
|
+
expect(data[0].example.attributes.c.assetType).to.not.exist;
|
|
46
46
|
});
|
|
47
47
|
});
|
|
@@ -40,7 +40,7 @@ function runTests(fileName) {
|
|
|
40
40
|
expect(data[0].example.attributes.b.type).to.equal('asset');
|
|
41
41
|
expect(data[0].example.attributes.b.array).to.equal(false);
|
|
42
42
|
expect(data[0].example.attributes.b.default).to.equal(null);
|
|
43
|
-
expect(data[0].example.attributes.b.
|
|
43
|
+
expect(data[0].example.attributes.b.assetType).to.equal('texture');
|
|
44
44
|
});
|
|
45
45
|
|
|
46
46
|
it('c: should be a asset attribute array with a container resource', function () {
|
|
@@ -49,7 +49,7 @@ function runTests(fileName) {
|
|
|
49
49
|
expect(data[0].example.attributes.c.type).to.equal('asset');
|
|
50
50
|
expect(data[0].example.attributes.c.array).to.equal(true);
|
|
51
51
|
expect(data[0].example.attributes.c.default).to.equal(null);
|
|
52
|
-
expect(data[0].example.attributes.c.
|
|
52
|
+
expect(data[0].example.attributes.c.assetType).to.equal('container');
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
});
|
|
@@ -37,12 +37,15 @@ describe('JS: VALID: Asset attribute', function () {
|
|
|
37
37
|
expect(data[0].example.attributes.f.schema[0].type).to.equal('boolean');
|
|
38
38
|
expect(data[0].example.attributes.f.schema[0].array).to.equal(false);
|
|
39
39
|
expect(data[0].example.attributes.f.schema[0].default).to.equal(false);
|
|
40
|
+
expect(data[0].example.attributes.f.schema[0].description).to.equal('This is some description of the attribute.');
|
|
40
41
|
});
|
|
41
42
|
|
|
42
43
|
it('f[b]: should be a numeric attribute', function () {
|
|
43
44
|
expect(data[0].example.attributes.f.schema[1].name).to.equal('b');
|
|
44
45
|
expect(data[0].example.attributes.f.schema[1].type).to.equal('number');
|
|
45
46
|
expect(data[0].example.attributes.f.schema[1].array).to.equal(false);
|
|
47
|
+
expect(data[0].example.attributes.f.schema[1].min).to.equal(10);
|
|
48
|
+
expect(data[0].example.attributes.f.schema[1].max).to.equal(20);
|
|
46
49
|
expect(data[0].example.attributes.f.schema[1].default).to.equal(10);
|
|
47
50
|
});
|
|
48
51
|
|
|
@@ -100,6 +103,22 @@ describe('JS: VALID: Asset attribute', function () {
|
|
|
100
103
|
expect(data[0].example.attributes.i.schema).to.exist;
|
|
101
104
|
});
|
|
102
105
|
|
|
106
|
+
it('i[a]: should be a boolean attribute', function () {
|
|
107
|
+
expect(data[0].example.attributes.i.schema[0].name).to.equal('a');
|
|
108
|
+
expect(data[0].example.attributes.i.schema[0].type).to.equal('boolean');
|
|
109
|
+
expect(data[0].example.attributes.i.schema[0].array).to.equal(false);
|
|
110
|
+
expect(data[0].example.attributes.i.schema[0].default).to.equal(true);
|
|
111
|
+
expect(data[0].example.attributes.i.schema[0].description).to.equal('description of a');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('i[b]: should be a numeric attribute', function () {
|
|
115
|
+
expect(data[0].example.attributes.i.schema[1].name).to.equal('b');
|
|
116
|
+
expect(data[0].example.attributes.i.schema[1].type).to.equal('number');
|
|
117
|
+
expect(data[0].example.attributes.i.schema[1].array).to.equal(false);
|
|
118
|
+
expect(data[0].example.attributes.i.schema[1].min).to.equal(10);
|
|
119
|
+
expect(data[0].example.attributes.i.schema[1].max).to.equal(20);
|
|
120
|
+
});
|
|
121
|
+
|
|
103
122
|
it('j: should be a typedef json attribute', function () {
|
|
104
123
|
expect(data[0].example.attributes.j).to.exist;
|
|
105
124
|
expect(data[0].example.attributes.j.name).to.equal('j');
|
|
@@ -184,6 +203,7 @@ describe('TS: VALID: Asset attribute', function () {
|
|
|
184
203
|
expect(data[0].example.attributes.f.schema[0].type).to.equal('boolean');
|
|
185
204
|
expect(data[0].example.attributes.f.schema[0].array).to.equal(false);
|
|
186
205
|
expect(data[0].example.attributes.f.schema[0].default).to.equal(false);
|
|
206
|
+
expect(data[0].example.attributes.f.schema[0].description).to.equal('This is some description of the attribute.');
|
|
187
207
|
});
|
|
188
208
|
|
|
189
209
|
it('f[b]: should be a numeric attribute', function () {
|
|
@@ -191,6 +211,8 @@ describe('TS: VALID: Asset attribute', function () {
|
|
|
191
211
|
expect(data[0].example.attributes.f.schema[1].type).to.equal('number');
|
|
192
212
|
expect(data[0].example.attributes.f.schema[1].array).to.equal(false);
|
|
193
213
|
expect(data[0].example.attributes.f.schema[1].default).to.equal(10);
|
|
214
|
+
expect(data[0].example.attributes.f.schema[1].min).to.equal(10);
|
|
215
|
+
expect(data[0].example.attributes.f.schema[1].max).to.equal(20);
|
|
194
216
|
});
|
|
195
217
|
|
|
196
218
|
it('f[c]: should be a string attribute', function () {
|
|
@@ -247,6 +269,22 @@ describe('TS: VALID: Asset attribute', function () {
|
|
|
247
269
|
expect(data[0].example.attributes.i.schema).to.exist;
|
|
248
270
|
});
|
|
249
271
|
|
|
272
|
+
it('i[a]: should be a boolean attribute', function () {
|
|
273
|
+
expect(data[0].example.attributes.i.schema[0].name).to.equal('a');
|
|
274
|
+
expect(data[0].example.attributes.i.schema[0].type).to.equal('boolean');
|
|
275
|
+
expect(data[0].example.attributes.i.schema[0].array).to.equal(false);
|
|
276
|
+
expect(data[0].example.attributes.i.schema[0].default).to.equal(true);
|
|
277
|
+
expect(data[0].example.attributes.i.schema[0].description).to.equal('description of a');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('i[b]: should be a numeric attribute', function () {
|
|
281
|
+
expect(data[0].example.attributes.i.schema[1].name).to.equal('b');
|
|
282
|
+
expect(data[0].example.attributes.i.schema[1].type).to.equal('number');
|
|
283
|
+
expect(data[0].example.attributes.i.schema[1].array).to.equal(false);
|
|
284
|
+
expect(data[0].example.attributes.i.schema[1].min).to.equal(10);
|
|
285
|
+
expect(data[0].example.attributes.i.schema[1].max).to.equal(20);
|
|
286
|
+
});
|
|
287
|
+
|
|
250
288
|
it('l: should be an inline type json attribute', function () {
|
|
251
289
|
expect(data[0].example.attributes.l).to.exist;
|
|
252
290
|
expect(data[0].example.attributes.l.name).to.equal('l');
|
|
@@ -28,7 +28,7 @@ describe('VALID: Misc attribute types', function () {
|
|
|
28
28
|
expect(data[0].example.attributes.a.array).to.equal(false);
|
|
29
29
|
expect(data[0].example.attributes.a.default).to.equal(0);
|
|
30
30
|
expect(data[0].example.attributes.a.title).to.equal('A number');
|
|
31
|
-
expect(data[0].example.attributes.a.description).to.equal('This is a description of the attribute');
|
|
31
|
+
expect(data[0].example.attributes.a.description).to.equal('This is a description of the attribute. It can be multi-sentence.\n\nAnd multi-line too');
|
|
32
32
|
});
|
|
33
33
|
|
|
34
34
|
it('b: should be a numeric attribute with a getter and setter', function () {
|
|
@@ -9,10 +9,15 @@ describe('VALID: Program ', function () {
|
|
|
9
9
|
data = await parseAttributes('./program.valid.js');
|
|
10
10
|
});
|
|
11
11
|
|
|
12
|
+
it('should have a customExample script', function () {
|
|
13
|
+
expect(data[0].customExample).to.exist;
|
|
14
|
+
expect(data[0].customExample.errors).to.be.empty;
|
|
15
|
+
});
|
|
16
|
+
|
|
12
17
|
it('only results should exist', function () {
|
|
13
18
|
expect(data).to.exist;
|
|
14
19
|
expect(data[0]).to.not.be.empty;
|
|
15
20
|
expect(data[1]).to.be.empty;
|
|
16
|
-
expect(data[0].
|
|
21
|
+
expect(data[0].customExample.errors).to.be.empty;
|
|
17
22
|
});
|
|
18
23
|
});
|