@lokascript/framework 2.3.1 → 2.5.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/dist/api/index.js +29 -516
- package/dist/api/index.js.map +1 -1
- package/dist/core/index.js +20 -128
- package/dist/core/index.js.map +1 -1
- package/dist/core/pattern-matching/index.js +19 -12
- package/dist/core/pattern-matching/index.js.map +1 -1
- package/dist/core/tokenization/index.js.map +1 -1
- package/dist/core/types.d.ts +7 -354
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +21 -129
- package/dist/core/types.js.map +1 -1
- package/dist/generation/diagnostics.d.ts +5 -115
- package/dist/generation/diagnostics.d.ts.map +1 -1
- package/dist/generation/index.js +20 -77
- package/dist/generation/index.js.map +1 -1
- package/dist/index.cjs +189 -1458
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +108 -1347
- package/dist/index.js.map +1 -1
- package/dist/interfaces/value-extractor.d.ts +20 -0
- package/dist/interfaces/value-extractor.d.ts.map +1 -1
- package/dist/ir/explicit-parser.d.ts +2 -83
- package/dist/ir/explicit-parser.d.ts.map +1 -1
- package/dist/ir/explicit-renderer.d.ts +2 -31
- package/dist/ir/explicit-renderer.d.ts.map +1 -1
- package/dist/ir/index.js +89 -1260
- package/dist/ir/index.js.map +1 -1
- package/dist/ir/protocol-json.d.ts +2 -70
- package/dist/ir/protocol-json.d.ts.map +1 -1
- package/dist/ir/references.d.ts +2 -16
- package/dist/ir/references.d.ts.map +1 -1
- package/dist/ir/types.d.ts +2 -148
- package/dist/ir/types.d.ts.map +1 -1
- package/dist/parsing/index.js +19 -12
- package/dist/parsing/index.js.map +1 -1
- package/dist/schema/command-schema.d.ts +3 -92
- package/dist/schema/command-schema.d.ts.map +1 -1
- package/dist/schema/index.js +3 -20
- package/dist/schema/index.js.map +1 -1
- package/dist/testing/index.js +1066 -712
- package/dist/testing/index.js.map +1 -1
- package/package.json +6 -2
- package/src/core/types.ts +71 -600
- package/src/generation/diagnostics.ts +12 -233
- package/src/interfaces/value-extractor.ts +32 -0
- package/src/ir/explicit-parser.ts +12 -792
- package/src/ir/explicit-renderer.ts +2 -195
- package/src/ir/from-interchange.test.ts +91 -4
- package/src/ir/from-interchange.ts +59 -0
- package/src/ir/protocol-json.test.ts +16 -7
- package/src/ir/protocol-json.ts +9 -748
- package/src/ir/references.ts +2 -30
- package/src/ir/types.ts +18 -190
- package/src/schema/command-schema.ts +3 -134
|
@@ -1,198 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Explicit Syntax Renderer
|
|
3
|
-
*
|
|
4
|
-
* Serializes SemanticNode to the universal [command role:value ...] bracket syntax.
|
|
5
|
-
* Zero dependencies beyond core types — no language-specific logic.
|
|
6
|
-
*
|
|
7
|
-
* Also renders annotations (@name(value)) and multi-line documents.
|
|
2
|
+
* Explicit Syntax Renderer — re-exported from @lokascript/intent
|
|
8
3
|
*/
|
|
9
4
|
|
|
10
|
-
|
|
11
|
-
SemanticNode,
|
|
12
|
-
SemanticValue,
|
|
13
|
-
CommandSemanticNode,
|
|
14
|
-
CompoundSemanticNode,
|
|
15
|
-
EventHandlerSemanticNode,
|
|
16
|
-
ConditionalSemanticNode,
|
|
17
|
-
LoopSemanticNode,
|
|
18
|
-
LSEEnvelope,
|
|
19
|
-
} from '../core/types';
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Render a semantic node as explicit bracket syntax.
|
|
23
|
-
*
|
|
24
|
-
* Handles annotations (v1.2) — if the node has annotations, they are
|
|
25
|
-
* prepended as @name or @name(value) before the bracket output.
|
|
26
|
-
*
|
|
27
|
-
* @example
|
|
28
|
-
* ```typescript
|
|
29
|
-
* renderExplicit(node) // "[toggle patient:.active destination:#button]"
|
|
30
|
-
* // With annotations:
|
|
31
|
-
* renderExplicit(annotatedNode) // "@timeout(5s) [fetch source:\"/api/users\"]"
|
|
32
|
-
* ```
|
|
33
|
-
*/
|
|
34
|
-
export function renderExplicit(node: SemanticNode): string {
|
|
35
|
-
let result: string;
|
|
36
|
-
|
|
37
|
-
// Handle compound nodes
|
|
38
|
-
if (node.kind === 'compound') {
|
|
39
|
-
const compoundNode = node as CompoundSemanticNode;
|
|
40
|
-
const renderedStatements = compoundNode.statements.map(stmt => renderExplicit(stmt));
|
|
41
|
-
result = renderedStatements.join(` ${compoundNode.chainType} `);
|
|
42
|
-
} else {
|
|
43
|
-
result = renderBracketCommand(node);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Prepend annotations if present
|
|
47
|
-
if (node.annotations && node.annotations.length > 0) {
|
|
48
|
-
const annParts = node.annotations.map(ann =>
|
|
49
|
-
ann.value !== undefined ? `@${ann.name}(${ann.value})` : `@${ann.name}`
|
|
50
|
-
);
|
|
51
|
-
return annParts.join(' ') + ' ' + result;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return result;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Render an LSEEnvelope as a multi-line document string.
|
|
59
|
-
*
|
|
60
|
-
* @example
|
|
61
|
-
* ```typescript
|
|
62
|
-
* renderDocument(envelope)
|
|
63
|
-
* // "#!lse 1.2\n[toggle patient:.active]\n[add patient:.highlight]"
|
|
64
|
-
* ```
|
|
65
|
-
*/
|
|
66
|
-
export function renderDocument(envelope: LSEEnvelope): string {
|
|
67
|
-
const lines: string[] = [];
|
|
68
|
-
|
|
69
|
-
// Emit version header if not the default
|
|
70
|
-
if (envelope.lseVersion && envelope.lseVersion !== '1.0') {
|
|
71
|
-
lines.push(`#!lse ${envelope.lseVersion}`);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Emit each node on its own line
|
|
75
|
-
for (const node of envelope.nodes) {
|
|
76
|
-
lines.push(renderExplicit(node));
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return lines.join('\n');
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// =============================================================================
|
|
83
|
-
// Internal Helpers
|
|
84
|
-
// =============================================================================
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Render a single bracket command (non-compound node).
|
|
88
|
-
*/
|
|
89
|
-
function renderBracketCommand(node: SemanticNode): string {
|
|
90
|
-
const parts: string[] = [node.action];
|
|
91
|
-
|
|
92
|
-
// Add roles
|
|
93
|
-
for (const [role, value] of node.roles) {
|
|
94
|
-
if (value.type === 'flag') {
|
|
95
|
-
parts.push(value.enabled ? `+${value.name}` : `~${value.name}`);
|
|
96
|
-
} else {
|
|
97
|
-
parts.push(`${role}:${valueToString(value)}`);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Handle event handler body
|
|
102
|
-
if (node.kind === 'event-handler') {
|
|
103
|
-
const eventNode = node as EventHandlerSemanticNode;
|
|
104
|
-
if (eventNode.body && eventNode.body.length > 0) {
|
|
105
|
-
const bodyParts = eventNode.body.map(n => renderExplicit(n));
|
|
106
|
-
parts.push(`body:${bodyParts.join(' ')}`);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Handle conditional branches (v1.1)
|
|
111
|
-
if (node.kind === 'conditional') {
|
|
112
|
-
const condNode = node as ConditionalSemanticNode;
|
|
113
|
-
if (condNode.thenBranch && condNode.thenBranch.length > 0) {
|
|
114
|
-
const thenParts = condNode.thenBranch.map(n => renderExplicit(n));
|
|
115
|
-
parts.push(`then:${thenParts.join(' ')}`);
|
|
116
|
-
}
|
|
117
|
-
if (condNode.elseBranch && condNode.elseBranch.length > 0) {
|
|
118
|
-
const elseParts = condNode.elseBranch.map(n => renderExplicit(n));
|
|
119
|
-
parts.push(`else:${elseParts.join(' ')}`);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Handle loop body and metadata (v1.1)
|
|
124
|
-
if (node.kind === 'loop') {
|
|
125
|
-
const loopNode = node as LoopSemanticNode;
|
|
126
|
-
if (loopNode.loopVariant) {
|
|
127
|
-
parts.push(`loop-variant:${loopNode.loopVariant}`);
|
|
128
|
-
}
|
|
129
|
-
if (loopNode.loopVariable) {
|
|
130
|
-
parts.push(`variable:${loopNode.loopVariable}`);
|
|
131
|
-
}
|
|
132
|
-
if (loopNode.indexVariable) {
|
|
133
|
-
parts.push(`index-variable:${loopNode.indexVariable}`);
|
|
134
|
-
}
|
|
135
|
-
if (loopNode.body && loopNode.body.length > 0) {
|
|
136
|
-
const bodyParts = loopNode.body.map(n => renderExplicit(n));
|
|
137
|
-
parts.push(`loop-body:${bodyParts.join(' ')}`);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Handle v1.2 command fields (try/catch/finally, async, match)
|
|
142
|
-
if (node.kind === 'command') {
|
|
143
|
-
const cmd = node as CommandSemanticNode;
|
|
144
|
-
if (cmd.body && cmd.body.length > 0) {
|
|
145
|
-
const bodyParts = cmd.body.map(n => renderExplicit(n));
|
|
146
|
-
parts.push(`body:${bodyParts.join(' ')}`);
|
|
147
|
-
}
|
|
148
|
-
if (cmd.catchBranch && cmd.catchBranch.length > 0) {
|
|
149
|
-
const catchParts = cmd.catchBranch.map(n => renderExplicit(n));
|
|
150
|
-
parts.push(`catch:${catchParts.join(' ')}`);
|
|
151
|
-
}
|
|
152
|
-
if (cmd.finallyBranch && cmd.finallyBranch.length > 0) {
|
|
153
|
-
const finallyParts = cmd.finallyBranch.map(n => renderExplicit(n));
|
|
154
|
-
parts.push(`finally:${finallyParts.join(' ')}`);
|
|
155
|
-
}
|
|
156
|
-
if (cmd.asyncVariant) {
|
|
157
|
-
parts.push(`async-variant:${cmd.asyncVariant}`);
|
|
158
|
-
}
|
|
159
|
-
if (cmd.asyncBody && cmd.asyncBody.length > 0) {
|
|
160
|
-
const asyncParts = cmd.asyncBody.map(n => renderExplicit(n));
|
|
161
|
-
parts.push(`async-body:${asyncParts.join(' ')}`);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return `[${parts.join(' ')}]`;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Convert a semantic value to its explicit syntax string form.
|
|
170
|
-
*/
|
|
171
|
-
function valueToString(value: SemanticValue): string {
|
|
172
|
-
switch (value.type) {
|
|
173
|
-
case 'literal':
|
|
174
|
-
if (typeof value.value === 'string') {
|
|
175
|
-
// Quote strings that contain spaces or are explicitly typed as strings
|
|
176
|
-
if (value.dataType === 'string' || /\s/.test(value.value)) {
|
|
177
|
-
return `"${value.value}"`;
|
|
178
|
-
}
|
|
179
|
-
return value.value;
|
|
180
|
-
}
|
|
181
|
-
return String(value.value);
|
|
182
|
-
|
|
183
|
-
case 'selector':
|
|
184
|
-
return value.value;
|
|
185
|
-
|
|
186
|
-
case 'reference':
|
|
187
|
-
return value.value;
|
|
188
|
-
|
|
189
|
-
case 'property-path':
|
|
190
|
-
return `${valueToString(value.object)}'s ${value.property}`;
|
|
191
|
-
|
|
192
|
-
case 'expression':
|
|
193
|
-
return value.raw;
|
|
194
|
-
|
|
195
|
-
case 'flag':
|
|
196
|
-
return value.name;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
5
|
+
export { renderExplicit, renderDocument } from '@lokascript/intent';
|
|
@@ -427,6 +427,89 @@ describe('fromInterchangeNode: command conversion', () => {
|
|
|
427
427
|
});
|
|
428
428
|
});
|
|
429
429
|
|
|
430
|
+
it('infers destination role for scroll, skipping `to` marker', () => {
|
|
431
|
+
const node = fromInterchangeNode({
|
|
432
|
+
type: 'command',
|
|
433
|
+
name: 'scroll',
|
|
434
|
+
args: [
|
|
435
|
+
{ type: 'identifier', value: 'to', name: 'to' },
|
|
436
|
+
{ type: 'selector', value: '#target' },
|
|
437
|
+
],
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
expect(node.action).toBe('scroll');
|
|
441
|
+
expect(node.roles.get('destination')).toEqual({
|
|
442
|
+
type: 'selector',
|
|
443
|
+
value: '#target',
|
|
444
|
+
selectorKind: 'id',
|
|
445
|
+
});
|
|
446
|
+
expect(node.roles.has('patient')).toBe(false);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it('infers destination for scroll across position/behavior markers', () => {
|
|
450
|
+
const node = fromInterchangeNode({
|
|
451
|
+
type: 'command',
|
|
452
|
+
name: 'scroll',
|
|
453
|
+
args: [
|
|
454
|
+
{ type: 'identifier', value: 'to', name: 'to' },
|
|
455
|
+
{ type: 'identifier', value: 'bottom', name: 'bottom' },
|
|
456
|
+
{ type: 'identifier', value: 'of', name: 'of' },
|
|
457
|
+
{ type: 'selector', value: '#chat' },
|
|
458
|
+
{ type: 'identifier', value: 'smoothly', name: 'smoothly' },
|
|
459
|
+
],
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
expect(node.roles.get('destination')).toEqual({
|
|
463
|
+
type: 'selector',
|
|
464
|
+
value: '#chat',
|
|
465
|
+
selectorKind: 'id',
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it('infers patient role for push, skipping `url` marker', () => {
|
|
470
|
+
const node = fromInterchangeNode({
|
|
471
|
+
type: 'command',
|
|
472
|
+
name: 'push',
|
|
473
|
+
args: [
|
|
474
|
+
{ type: 'identifier', value: 'url', name: 'url' },
|
|
475
|
+
{ type: 'literal', value: '/page/2' },
|
|
476
|
+
],
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
expect(node.action).toBe('push');
|
|
480
|
+
expect(node.roles.get('patient')).toEqual({ type: 'literal', value: '/page/2' });
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it('infers patient role for replace, skipping `url` marker', () => {
|
|
484
|
+
const node = fromInterchangeNode({
|
|
485
|
+
type: 'command',
|
|
486
|
+
name: 'replace',
|
|
487
|
+
args: [
|
|
488
|
+
{ type: 'identifier', value: 'url', name: 'url' },
|
|
489
|
+
{ type: 'literal', value: '/new' },
|
|
490
|
+
],
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
expect(node.action).toBe('replace');
|
|
494
|
+
expect(node.roles.get('patient')).toEqual({ type: 'literal', value: '/new' });
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it('infers patient role for process, skipping `partials in` markers', () => {
|
|
498
|
+
const node = fromInterchangeNode({
|
|
499
|
+
type: 'command',
|
|
500
|
+
name: 'process',
|
|
501
|
+
args: [
|
|
502
|
+
{ type: 'identifier', value: 'partials', name: 'partials' },
|
|
503
|
+
{ type: 'identifier', value: 'in', name: 'in' },
|
|
504
|
+
{ type: 'identifier', value: 'it' },
|
|
505
|
+
],
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
expect(node.action).toBe('process');
|
|
509
|
+
// `it` is in DEFAULT_REFERENCES → wrapped as a reference value
|
|
510
|
+
expect(node.roles.get('patient')).toEqual({ type: 'reference', value: 'it' });
|
|
511
|
+
});
|
|
512
|
+
|
|
430
513
|
it('wraps bare expression nodes as get command', () => {
|
|
431
514
|
const node = fromInterchangeNode({
|
|
432
515
|
type: 'binary',
|
|
@@ -759,17 +842,21 @@ describe('round-trip: InterchangeNode → SemanticNode → protocol JSON', () =>
|
|
|
759
842
|
});
|
|
760
843
|
});
|
|
761
844
|
|
|
762
|
-
it('event handler produces valid protocol JSON', () => {
|
|
845
|
+
it('event handler produces valid protocol JSON (compact trigger form)', () => {
|
|
763
846
|
const node = fromInterchangeNode({
|
|
764
847
|
type: 'event',
|
|
765
848
|
event: 'click',
|
|
766
849
|
body: [{ type: 'command', name: 'log', args: [{ type: 'literal', value: 'clicked' }] }],
|
|
767
850
|
});
|
|
768
851
|
|
|
852
|
+
// Single-command bodies are emitted in the compact form — the body
|
|
853
|
+
// command's action is hoisted to the top level and the event name moves
|
|
854
|
+
// under `trigger.event`. See protocol.ts:canEmitCompactTrigger.
|
|
769
855
|
const proto = toProtocolJSON(node);
|
|
770
|
-
expect(proto.kind).
|
|
771
|
-
expect(proto.
|
|
772
|
-
expect(proto.
|
|
856
|
+
expect(proto.kind).toBeUndefined();
|
|
857
|
+
expect(proto.action).toBe('log');
|
|
858
|
+
expect(proto.trigger).toEqual({ event: 'click' });
|
|
859
|
+
expect(proto.body).toBeUndefined();
|
|
773
860
|
});
|
|
774
861
|
|
|
775
862
|
it('if/else produces valid protocol JSON', () => {
|
|
@@ -428,6 +428,43 @@ export function renderExpr(node: INode): string {
|
|
|
428
428
|
// Role Inference
|
|
429
429
|
// =============================================================================
|
|
430
430
|
|
|
431
|
+
/**
|
|
432
|
+
* Marker-keyword sets — mirror the runtime command parsers' skip lists so the
|
|
433
|
+
* inferred roles point at the real value, not the literal marker word.
|
|
434
|
+
*/
|
|
435
|
+
const SCROLL_MARKERS: ReadonlySet<string> = new Set([
|
|
436
|
+
'to',
|
|
437
|
+
'of',
|
|
438
|
+
'the',
|
|
439
|
+
'top',
|
|
440
|
+
'bottom',
|
|
441
|
+
'middle',
|
|
442
|
+
'center',
|
|
443
|
+
'nearest',
|
|
444
|
+
'left',
|
|
445
|
+
'right',
|
|
446
|
+
'smoothly',
|
|
447
|
+
'instantly',
|
|
448
|
+
]);
|
|
449
|
+
const URL_MARKERS: ReadonlySet<string> = new Set(['url']);
|
|
450
|
+
const PARTIALS_IN_MARKERS: ReadonlySet<string> = new Set(['partials', 'in']);
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Return the first arg that isn't a marker identifier from the given set.
|
|
454
|
+
* Identifier args carry the marker word in `name` (or `value` as a fallback).
|
|
455
|
+
*/
|
|
456
|
+
function firstNonMarkerArg(args: INode[], markers: ReadonlySet<string>): INode | undefined {
|
|
457
|
+
for (const a of args) {
|
|
458
|
+
if (a.type === 'identifier') {
|
|
459
|
+
const ident = a as { name?: unknown; value?: unknown };
|
|
460
|
+
const word = typeof ident.name === 'string' ? ident.name : ident.value;
|
|
461
|
+
if (typeof word === 'string' && markers.has(word)) continue;
|
|
462
|
+
}
|
|
463
|
+
return a;
|
|
464
|
+
}
|
|
465
|
+
return undefined;
|
|
466
|
+
}
|
|
467
|
+
|
|
431
468
|
/**
|
|
432
469
|
* Infer semantic roles from command name, positional args, modifiers, and target.
|
|
433
470
|
* Mirrors the heuristics in core's from-core.ts inferRoles() function.
|
|
@@ -526,6 +563,28 @@ function inferRoles(
|
|
|
526
563
|
break;
|
|
527
564
|
}
|
|
528
565
|
|
|
566
|
+
// scroll [to] [position] target [smoothly|instantly] → destination=target
|
|
567
|
+
case 'scroll': {
|
|
568
|
+
const t = firstNonMarkerArg(args, SCROLL_MARKERS);
|
|
569
|
+
if (t) roles.destination = convertValue(t);
|
|
570
|
+
break;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// push|replace url <X> → patient=X
|
|
574
|
+
case 'push':
|
|
575
|
+
case 'replace': {
|
|
576
|
+
const t = firstNonMarkerArg(args, URL_MARKERS);
|
|
577
|
+
if (t) roles.patient = convertValue(t);
|
|
578
|
+
break;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// process partials in <X> → patient=X
|
|
582
|
+
case 'process': {
|
|
583
|
+
const t = firstNonMarkerArg(args, PARTIALS_IN_MARKERS);
|
|
584
|
+
if (t) roles.patient = convertValue(t);
|
|
585
|
+
break;
|
|
586
|
+
}
|
|
587
|
+
|
|
529
588
|
default: {
|
|
530
589
|
// Generic fallback: first arg as patient, target as destination
|
|
531
590
|
if (args[0]) roles.patient = convertValue(args[0]);
|
|
@@ -136,20 +136,25 @@ describe('toProtocolJSON', () => {
|
|
|
136
136
|
});
|
|
137
137
|
|
|
138
138
|
describe('event-handler node', () => {
|
|
139
|
-
it('serializes body
|
|
139
|
+
it('serializes single-command body in compact form (trigger sugar)', () => {
|
|
140
|
+
// Single-command bodies round-trip through the compact form —
|
|
141
|
+
// `{action, roles, trigger: {event}}` — which is also what the runtime
|
|
142
|
+
// dispatch path reliably executes. See protocol.ts:canEmitCompactTrigger.
|
|
140
143
|
const body = createCommandNode('toggle', { patient: createSelector('.active') });
|
|
141
144
|
const node = createEventHandlerNode('on', { event: createLiteral('click', 'string') }, [
|
|
142
145
|
body,
|
|
143
146
|
]);
|
|
144
147
|
const json = toProtocolJSON(node);
|
|
145
|
-
expect(json.kind).
|
|
146
|
-
expect(json.action).toBe('
|
|
147
|
-
expect(json.
|
|
148
|
-
expect(json.
|
|
149
|
-
expect(json.body
|
|
148
|
+
expect(json.kind).toBeUndefined(); // defaults to 'command'
|
|
149
|
+
expect(json.action).toBe('toggle'); // body action is hoisted
|
|
150
|
+
expect(json.trigger).toEqual({ event: 'click' });
|
|
151
|
+
expect(json.roles).toMatchObject({ patient: { type: 'selector', value: '.active' } });
|
|
152
|
+
expect(json.body).toBeUndefined();
|
|
150
153
|
});
|
|
151
154
|
|
|
152
|
-
it('
|
|
155
|
+
it('falls back to verbose form for multi-command event-handler body', () => {
|
|
156
|
+
// Compact form can only represent a single body command; multi-command
|
|
157
|
+
// bodies still use the verbose `kind: event-handler` form.
|
|
153
158
|
const inner = createCommandNode('add', { patient: createSelector('.highlight') });
|
|
154
159
|
const outer = createCommandNode('remove', { patient: createSelector('.active') });
|
|
155
160
|
const node = createEventHandlerNode('on', { event: createLiteral('click', 'string') }, [
|
|
@@ -157,8 +162,12 @@ describe('toProtocolJSON', () => {
|
|
|
157
162
|
outer,
|
|
158
163
|
]);
|
|
159
164
|
const json = toProtocolJSON(node);
|
|
165
|
+
expect(json.kind).toBe('event-handler');
|
|
166
|
+
expect(json.action).toBe('on');
|
|
160
167
|
expect(json.body).toHaveLength(2);
|
|
168
|
+
expect(json.body![0].action).toBe('add');
|
|
161
169
|
expect(json.body![1].action).toBe('remove');
|
|
170
|
+
expect(json.trigger).toBeUndefined();
|
|
162
171
|
});
|
|
163
172
|
});
|
|
164
173
|
|