@daz4126/helium 0.31.0 → 1.0.0-beta.2
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/.claude/settings.local.json +3 -1
- package/CLAUDE.md +1 -1
- package/examples/index-lite.html +396 -0
- package/examples/index.html +27 -11
- package/examples/password-utils.js +80 -0
- package/helium-csp.js +252 -0
- package/helium-csp.test.js +776 -0
- package/helium-lite.js +369 -0
- package/helium-lite.test.js +120 -0
- package/helium.js +35 -9
- package/jexpr.js +666 -0
- package/jexpr.test.js +551 -0
- package/package.json +6 -1
- package/test-csp.html +50 -0
package/jexpr.test.js
ADDED
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { parse, EvalAstFactory } from './jexpr.js';
|
|
3
|
+
import { evaluate as heliumEvaluate } from './helium-csp.js';
|
|
4
|
+
|
|
5
|
+
const astFactory = new EvalAstFactory();
|
|
6
|
+
const evaluate = (expr, scope = {}) => {
|
|
7
|
+
const ast = parse(expr, astFactory);
|
|
8
|
+
return ast.evaluate(scope);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
describe('jexpr - Expression Parser', () => {
|
|
12
|
+
|
|
13
|
+
describe('Literals', () => {
|
|
14
|
+
it('should parse integers', () => {
|
|
15
|
+
expect(evaluate('42')).toBe(42);
|
|
16
|
+
expect(evaluate('0')).toBe(0);
|
|
17
|
+
expect(evaluate('-5')).toBe(-5);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should parse decimals', () => {
|
|
21
|
+
expect(evaluate('3.14')).toBe(3.14);
|
|
22
|
+
expect(evaluate('0.5')).toBe(0.5);
|
|
23
|
+
expect(evaluate('-2.5')).toBe(-2.5);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should parse strings with single quotes', () => {
|
|
27
|
+
expect(evaluate("'hello'")).toBe('hello');
|
|
28
|
+
expect(evaluate("'hello world'")).toBe('hello world');
|
|
29
|
+
expect(evaluate("''")).toBe('');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should parse strings with double quotes', () => {
|
|
33
|
+
expect(evaluate('"hello"')).toBe('hello');
|
|
34
|
+
expect(evaluate('"hello world"')).toBe('hello world');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should handle escape sequences in strings', () => {
|
|
38
|
+
expect(evaluate("'hello\\nworld'")).toBe('hello\nworld');
|
|
39
|
+
expect(evaluate("'tab\\there'")).toBe('tab\there');
|
|
40
|
+
expect(evaluate("'quote\\'s'")).toBe("quote's");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should parse boolean literals', () => {
|
|
44
|
+
expect(evaluate('true')).toBe(true);
|
|
45
|
+
expect(evaluate('false')).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should parse null and undefined', () => {
|
|
49
|
+
expect(evaluate('null')).toBe(null);
|
|
50
|
+
expect(evaluate('undefined')).toBe(undefined);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('Identifiers', () => {
|
|
55
|
+
it('should resolve identifiers from scope', () => {
|
|
56
|
+
expect(evaluate('x', { x: 10 })).toBe(10);
|
|
57
|
+
expect(evaluate('name', { name: 'Alice' })).toBe('Alice');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should return undefined for missing identifiers', () => {
|
|
61
|
+
expect(evaluate('missing', {})).toBe(undefined);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should handle identifiers starting with $', () => {
|
|
65
|
+
expect(evaluate('$data', { $data: 'value' })).toBe('value');
|
|
66
|
+
expect(evaluate('$el', { $el: 'element' })).toBe('element');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should handle identifiers with underscores', () => {
|
|
70
|
+
expect(evaluate('_private', { _private: 'secret' })).toBe('secret');
|
|
71
|
+
expect(evaluate('my_var', { my_var: 123 })).toBe(123);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('Property Access', () => {
|
|
76
|
+
it('should access object properties with dot notation', () => {
|
|
77
|
+
expect(evaluate('obj.name', { obj: { name: 'test' } })).toBe('test');
|
|
78
|
+
expect(evaluate('user.age', { user: { age: 25 } })).toBe(25);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should access nested properties', () => {
|
|
82
|
+
const scope = { a: { b: { c: 'deep' } } };
|
|
83
|
+
expect(evaluate('a.b.c', scope)).toBe('deep');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should access array elements with bracket notation', () => {
|
|
87
|
+
expect(evaluate('arr[0]', { arr: ['first', 'second'] })).toBe('first');
|
|
88
|
+
expect(evaluate('arr[1]', { arr: ['first', 'second'] })).toBe('second');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should access object properties with bracket notation', () => {
|
|
92
|
+
expect(evaluate("obj['key']", { obj: { key: 'value' } })).toBe('value');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should handle dynamic property access', () => {
|
|
96
|
+
expect(evaluate('obj[key]', { obj: { a: 1, b: 2 }, key: 'b' })).toBe(2);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should return undefined for missing properties', () => {
|
|
100
|
+
expect(evaluate('obj.missing', { obj: {} })).toBe(undefined);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should handle optional chaining style access safely', () => {
|
|
104
|
+
// jexpr uses ?. internally for safety
|
|
105
|
+
expect(evaluate('obj.prop', { obj: null })).toBe(undefined);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('Arithmetic Operators', () => {
|
|
110
|
+
it('should add numbers', () => {
|
|
111
|
+
expect(evaluate('1 + 2')).toBe(3);
|
|
112
|
+
expect(evaluate('10 + 20 + 30')).toBe(60);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should subtract numbers', () => {
|
|
116
|
+
expect(evaluate('5 - 3')).toBe(2);
|
|
117
|
+
expect(evaluate('10 - 3 - 2')).toBe(5);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should multiply numbers', () => {
|
|
121
|
+
expect(evaluate('3 * 4')).toBe(12);
|
|
122
|
+
expect(evaluate('2 * 3 * 4')).toBe(24);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should divide numbers', () => {
|
|
126
|
+
expect(evaluate('10 / 2')).toBe(5);
|
|
127
|
+
expect(evaluate('20 / 4 / 2')).toBe(2.5);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should calculate modulo', () => {
|
|
131
|
+
expect(evaluate('10 % 3')).toBe(1);
|
|
132
|
+
expect(evaluate('7 % 2')).toBe(1);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should respect operator precedence', () => {
|
|
136
|
+
expect(evaluate('2 + 3 * 4')).toBe(14);
|
|
137
|
+
expect(evaluate('10 - 4 / 2')).toBe(8);
|
|
138
|
+
expect(evaluate('2 * 3 + 4 * 5')).toBe(26);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should handle parentheses', () => {
|
|
142
|
+
expect(evaluate('(2 + 3) * 4')).toBe(20);
|
|
143
|
+
expect(evaluate('10 / (2 + 3)')).toBe(2);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Note: jexpr doesn't support prefix unary operators (-5, +5)
|
|
147
|
+
// Use 0 - x or scope variables for negative numbers
|
|
148
|
+
it('should handle negative numbers via subtraction', () => {
|
|
149
|
+
expect(evaluate('0 - 5')).toBe(-5);
|
|
150
|
+
expect(evaluate('10 - 5')).toBe(5);
|
|
151
|
+
expect(evaluate('n', { n: -5 })).toBe(-5);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('Comparison Operators', () => {
|
|
156
|
+
it('should compare equality with ==', () => {
|
|
157
|
+
expect(evaluate('1 == 1')).toBe(true);
|
|
158
|
+
expect(evaluate('1 == 2')).toBe(false);
|
|
159
|
+
expect(evaluate("1 == '1'")).toBe(true);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should compare strict equality with ===', () => {
|
|
163
|
+
expect(evaluate('1 === 1')).toBe(true);
|
|
164
|
+
expect(evaluate("1 === '1'")).toBe(false);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should compare inequality with !=', () => {
|
|
168
|
+
expect(evaluate('1 != 2')).toBe(true);
|
|
169
|
+
expect(evaluate('1 != 1')).toBe(false);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should compare strict inequality with !==', () => {
|
|
173
|
+
expect(evaluate("1 !== '1'")).toBe(true);
|
|
174
|
+
expect(evaluate('1 !== 1')).toBe(false);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should compare greater than', () => {
|
|
178
|
+
expect(evaluate('5 > 3')).toBe(true);
|
|
179
|
+
expect(evaluate('3 > 5')).toBe(false);
|
|
180
|
+
expect(evaluate('3 > 3')).toBe(false);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should compare greater than or equal', () => {
|
|
184
|
+
expect(evaluate('5 >= 3')).toBe(true);
|
|
185
|
+
expect(evaluate('3 >= 3')).toBe(true);
|
|
186
|
+
expect(evaluate('2 >= 3')).toBe(false);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should compare less than', () => {
|
|
190
|
+
expect(evaluate('3 < 5')).toBe(true);
|
|
191
|
+
expect(evaluate('5 < 3')).toBe(false);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should compare less than or equal', () => {
|
|
195
|
+
expect(evaluate('3 <= 5')).toBe(true);
|
|
196
|
+
expect(evaluate('3 <= 3')).toBe(true);
|
|
197
|
+
expect(evaluate('5 <= 3')).toBe(false);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('Logical Operators', () => {
|
|
202
|
+
it('should evaluate logical AND', () => {
|
|
203
|
+
expect(evaluate('true && true')).toBe(true);
|
|
204
|
+
expect(evaluate('true && false')).toBe(false);
|
|
205
|
+
expect(evaluate('false && true')).toBe(false);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should evaluate logical OR', () => {
|
|
209
|
+
expect(evaluate('true || false')).toBe(true);
|
|
210
|
+
expect(evaluate('false || true')).toBe(true);
|
|
211
|
+
expect(evaluate('false || false')).toBe(false);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Note: jexpr doesn't support prefix ! operator
|
|
215
|
+
// Use ternary or comparison for negation: x ? false : true, or x == false
|
|
216
|
+
it('should handle negation via ternary', () => {
|
|
217
|
+
expect(evaluate('x ? false : true', { x: true })).toBe(false);
|
|
218
|
+
expect(evaluate('x ? false : true', { x: false })).toBe(true);
|
|
219
|
+
expect(evaluate('x == false', { x: true })).toBe(false);
|
|
220
|
+
expect(evaluate('x == false', { x: false })).toBe(true);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('should evaluate nullish coalescing', () => {
|
|
224
|
+
expect(evaluate('null ?? "default"')).toBe('default');
|
|
225
|
+
expect(evaluate('undefined ?? "default"')).toBe('default');
|
|
226
|
+
expect(evaluate('"value" ?? "default"')).toBe('value');
|
|
227
|
+
expect(evaluate('0 ?? "default"')).toBe(0);
|
|
228
|
+
expect(evaluate('false ?? "default"')).toBe(false);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('should short-circuit AND', () => {
|
|
232
|
+
expect(evaluate('false && x', { x: 'not evaluated' })).toBe(false);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('should short-circuit OR', () => {
|
|
236
|
+
expect(evaluate('true || x', { x: 'not evaluated' })).toBe(true);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe('Ternary Operator', () => {
|
|
241
|
+
it('should evaluate ternary with true condition', () => {
|
|
242
|
+
expect(evaluate("true ? 'yes' : 'no'")).toBe('yes');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('should evaluate ternary with false condition', () => {
|
|
246
|
+
expect(evaluate("false ? 'yes' : 'no'")).toBe('no');
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('should evaluate ternary with expression condition', () => {
|
|
250
|
+
expect(evaluate("5 > 3 ? 'bigger' : 'smaller'")).toBe('bigger');
|
|
251
|
+
expect(evaluate("2 > 3 ? 'bigger' : 'smaller'")).toBe('smaller');
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('should handle nested ternary', () => {
|
|
255
|
+
expect(evaluate("true ? (false ? 'a' : 'b') : 'c'")).toBe('b');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('should use scope values in ternary', () => {
|
|
259
|
+
expect(evaluate("x > 0 ? 'positive' : 'non-positive'", { x: 5 })).toBe('positive');
|
|
260
|
+
expect(evaluate("x > 0 ? 'positive' : 'non-positive'", { x: -5 })).toBe('non-positive');
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
describe('Function Calls', () => {
|
|
265
|
+
it('should call functions from scope', () => {
|
|
266
|
+
const scope = { double: x => x * 2 };
|
|
267
|
+
expect(evaluate('double(5)', scope)).toBe(10);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('should call functions with multiple arguments', () => {
|
|
271
|
+
const scope = { add: (a, b) => a + b };
|
|
272
|
+
expect(evaluate('add(3, 4)', scope)).toBe(7);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('should call functions with no arguments', () => {
|
|
276
|
+
const scope = { getValue: () => 42 };
|
|
277
|
+
expect(evaluate('getValue()', scope)).toBe(42);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('should call methods on objects', () => {
|
|
281
|
+
const scope = { str: 'hello' };
|
|
282
|
+
expect(evaluate('str.toUpperCase()', scope)).toBe('HELLO');
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('should call methods with arguments', () => {
|
|
286
|
+
const scope = { arr: [1, 2, 3] };
|
|
287
|
+
expect(evaluate('arr.includes(2)', scope)).toBe(true);
|
|
288
|
+
expect(evaluate('arr.includes(5)', scope)).toBe(false);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('should chain method calls', () => {
|
|
292
|
+
const scope = { str: ' hello ' };
|
|
293
|
+
expect(evaluate('str.trim().toUpperCase()', scope)).toBe('HELLO');
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('should handle Math functions', () => {
|
|
297
|
+
const scope = { Math };
|
|
298
|
+
expect(evaluate('Math.abs(-5)', scope)).toBe(5);
|
|
299
|
+
expect(evaluate('Math.max(1, 5, 3)', scope)).toBe(5);
|
|
300
|
+
expect(evaluate('Math.round(3.7)', scope)).toBe(4);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('should handle console.log', () => {
|
|
304
|
+
const logs = [];
|
|
305
|
+
const scope = { console: { log: (...args) => logs.push(args) } };
|
|
306
|
+
evaluate("console.log('test')", scope);
|
|
307
|
+
expect(logs).toEqual([['test']]);
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
describe('Arrow Functions', () => {
|
|
312
|
+
it('should parse single-param arrow function without parens', () => {
|
|
313
|
+
const fn = evaluate('x => x * 2');
|
|
314
|
+
expect(fn(5)).toBe(10);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('should parse single-param arrow function with parens', () => {
|
|
318
|
+
const fn = evaluate('(x) => x * 2');
|
|
319
|
+
expect(fn(5)).toBe(10);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('should parse multi-param arrow function', () => {
|
|
323
|
+
const fn = evaluate('(a, b) => a + b');
|
|
324
|
+
expect(fn(3, 4)).toBe(7);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('should parse no-param arrow function', () => {
|
|
328
|
+
const fn = evaluate('() => 42');
|
|
329
|
+
expect(fn()).toBe(42);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('should capture scope in arrow function', () => {
|
|
333
|
+
const scope = { multiplier: 10 };
|
|
334
|
+
const fn = evaluate('x => x * multiplier', scope);
|
|
335
|
+
expect(fn(5)).toBe(50);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('should work as callback in map-like usage', () => {
|
|
339
|
+
const scope = {
|
|
340
|
+
items: [1, 2, 3],
|
|
341
|
+
map: (arr, fn) => arr.map(fn)
|
|
342
|
+
};
|
|
343
|
+
const result = evaluate('map(items, x => x * 2)', scope);
|
|
344
|
+
expect(result).toEqual([2, 4, 6]);
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
describe('Array Literals', () => {
|
|
349
|
+
it('should parse empty array', () => {
|
|
350
|
+
expect(evaluate('[]')).toEqual([]);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('should parse array with values', () => {
|
|
354
|
+
expect(evaluate('[1, 2, 3]')).toEqual([1, 2, 3]);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('should parse array with mixed types', () => {
|
|
358
|
+
expect(evaluate("[1, 'two', true]")).toEqual([1, 'two', true]);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('should parse nested arrays', () => {
|
|
362
|
+
expect(evaluate('[[1, 2], [3, 4]]')).toEqual([[1, 2], [3, 4]]);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('should parse array with expressions', () => {
|
|
366
|
+
expect(evaluate('[1 + 1, 2 * 2, 3]')).toEqual([2, 4, 3]);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('should parse array with scope values', () => {
|
|
370
|
+
expect(evaluate('[x, y]', { x: 10, y: 20 })).toEqual([10, 20]);
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
describe('Object Literals (Maps)', () => {
|
|
375
|
+
it('should parse empty object', () => {
|
|
376
|
+
expect(evaluate('{}')).toEqual({});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it('should parse object with properties', () => {
|
|
380
|
+
expect(evaluate('{a: 1, b: 2}')).toEqual({ a: 1, b: 2 });
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('should parse object with string keys', () => {
|
|
384
|
+
expect(evaluate("{'key': 'value'}")).toEqual({ key: 'value' });
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('should parse object with expression values', () => {
|
|
388
|
+
expect(evaluate('{sum: 1 + 2, product: 3 * 4}')).toEqual({ sum: 3, product: 12 });
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it('should parse object with scope values', () => {
|
|
392
|
+
expect(evaluate('{x: x, y: y}', { x: 10, y: 20 })).toEqual({ x: 10, y: 20 });
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it('should parse nested objects', () => {
|
|
396
|
+
expect(evaluate('{outer: {inner: 42}}')).toEqual({ outer: { inner: 42 } });
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
describe('Assignment', () => {
|
|
401
|
+
it('should assign to scope variables', () => {
|
|
402
|
+
const scope = { x: 0 };
|
|
403
|
+
evaluate('x = 10', scope);
|
|
404
|
+
expect(scope.x).toBe(10);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it('should assign to object properties', () => {
|
|
408
|
+
const scope = { obj: { value: 0 } };
|
|
409
|
+
evaluate('obj.value = 42', scope);
|
|
410
|
+
expect(scope.obj.value).toBe(42);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it('should assign to array indices', () => {
|
|
414
|
+
const scope = { arr: [1, 2, 3] };
|
|
415
|
+
evaluate('arr[1] = 99', scope);
|
|
416
|
+
expect(scope.arr[1]).toBe(99);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('should return assigned value', () => {
|
|
420
|
+
const scope = { x: 0 };
|
|
421
|
+
expect(evaluate('x = 10', scope)).toBe(10);
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
describe('String Concatenation', () => {
|
|
426
|
+
it('should concatenate strings', () => {
|
|
427
|
+
expect(evaluate("'hello' + ' ' + 'world'")).toBe('hello world');
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('should concatenate strings and numbers', () => {
|
|
431
|
+
expect(evaluate("'count: ' + 42")).toBe('count: 42');
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it('should concatenate with scope values', () => {
|
|
435
|
+
expect(evaluate("'Hello, ' + name + '!'", { name: 'Alice' })).toBe('Hello, Alice!');
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
describe('Complex Expressions', () => {
|
|
440
|
+
it('should handle complex arithmetic', () => {
|
|
441
|
+
expect(evaluate('((2 + 3) * 4 - 10) / 2')).toBe(5);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it('should handle complex boolean logic', () => {
|
|
445
|
+
expect(evaluate('(true && false) || (true && true)')).toBe(true);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it('should handle complex property access and calls', () => {
|
|
449
|
+
const scope = {
|
|
450
|
+
users: [
|
|
451
|
+
{ name: 'Alice', age: 30 },
|
|
452
|
+
{ name: 'Bob', age: 25 }
|
|
453
|
+
]
|
|
454
|
+
};
|
|
455
|
+
expect(evaluate('users[0].name', scope)).toBe('Alice');
|
|
456
|
+
expect(evaluate('users[1].age', scope)).toBe(25);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('should handle chained operations', () => {
|
|
460
|
+
const scope = {
|
|
461
|
+
data: {
|
|
462
|
+
items: [{ value: 10 }, { value: 20 }]
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
expect(evaluate('data.items[1].value * 2', scope)).toBe(40);
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
describe('getIds - Dependency Tracking', () => {
|
|
470
|
+
it('should return identifiers from simple expression', () => {
|
|
471
|
+
const ast = parse('x + y', astFactory);
|
|
472
|
+
expect(ast.getIds([])).toContain('x');
|
|
473
|
+
expect(ast.getIds([])).toContain('y');
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it('should return identifier from property access', () => {
|
|
477
|
+
const ast = parse('obj.prop', astFactory);
|
|
478
|
+
expect(ast.getIds([])).toContain('obj');
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it('should return identifiers from function call', () => {
|
|
482
|
+
const ast = parse('fn(a, b)', astFactory);
|
|
483
|
+
expect(ast.getIds([])).toContain('fn');
|
|
484
|
+
expect(ast.getIds([])).toContain('a');
|
|
485
|
+
expect(ast.getIds([])).toContain('b');
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it('should not include arrow function params as external deps', () => {
|
|
489
|
+
const ast = parse('x => x * multiplier', astFactory);
|
|
490
|
+
const ids = ast.getIds([]);
|
|
491
|
+
expect(ids).not.toContain('x');
|
|
492
|
+
expect(ids).toContain('multiplier');
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
describe('Error Handling', () => {
|
|
497
|
+
it('should throw on unterminated string', () => {
|
|
498
|
+
expect(() => parse("'unterminated", astFactory)).toThrow();
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it('should throw on unexpected token', () => {
|
|
502
|
+
expect(() => parse('1 + + +', astFactory)).toThrow();
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it('should throw on invalid assignment target', () => {
|
|
506
|
+
const ast = parse('1 = 2', astFactory);
|
|
507
|
+
expect(() => ast.evaluate({})).toThrow();
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
describe('Template Literal Preprocessing', () => {
|
|
512
|
+
it('should transform simple template to string', () => {
|
|
513
|
+
// Test via heliumEvaluate which uses preprocess internally
|
|
514
|
+
expect(heliumEvaluate('`hello`', {})).toBe('hello');
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
it('should transform template with interpolation', () => {
|
|
518
|
+
expect(heliumEvaluate('`Hello ${name}!`', { name: 'World' })).toBe('Hello World!');
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it('should handle multiple interpolations', () => {
|
|
522
|
+
expect(heliumEvaluate('`${a} + ${b} = ${a + b}`', { a: 2, b: 3 })).toBe('2 + 3 = 5');
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
describe('Edge Cases', () => {
|
|
527
|
+
it('should handle empty expression', () => {
|
|
528
|
+
const ast = parse('', astFactory);
|
|
529
|
+
expect(ast.evaluate({})).toEqual({});
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
it('should handle whitespace-only expression', () => {
|
|
533
|
+
const ast = parse(' ', astFactory);
|
|
534
|
+
expect(ast.evaluate({})).toEqual({});
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
it('should handle deeply nested parentheses', () => {
|
|
538
|
+
expect(evaluate('(((1 + 2)))')).toBe(3);
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
it('should handle this keyword', () => {
|
|
542
|
+
const scope = { value: 42 };
|
|
543
|
+
expect(evaluate('this', scope)).toBe(scope);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it('should handle numbers with leading decimal', () => {
|
|
547
|
+
expect(evaluate('.5')).toBe(0.5);
|
|
548
|
+
expect(evaluate('-.5')).toBe(-0.5);
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
});
|
package/package.json
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@daz4126/helium",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0-beta.2",
|
|
4
4
|
"main": "helium.js",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./helium.js",
|
|
8
|
+
"./lite": "./helium-lite.js",
|
|
9
|
+
"./csp": "./helium-csp.js"
|
|
10
|
+
},
|
|
6
11
|
"scripts": {
|
|
7
12
|
"test": "vitest",
|
|
8
13
|
"test:ui": "vitest --ui",
|
package/test-csp.html
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Helium CSP Test</title>
|
|
7
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline';">
|
|
8
|
+
</head>
|
|
9
|
+
<body data-helium>
|
|
10
|
+
<div>
|
|
11
|
+
<h1 @text="message"></h1>
|
|
12
|
+
<button @click="message = 'Clicked!'">Click me</button>
|
|
13
|
+
<button @click="count++">Count: <span @text="count"></span></button>
|
|
14
|
+
<div @text="'Timer: ' + timer"></div>
|
|
15
|
+
<input @bind="message" placeholder="Type message">
|
|
16
|
+
|
|
17
|
+
<hr>
|
|
18
|
+
|
|
19
|
+
<h2>CSP Features</h2>
|
|
20
|
+
<div @text="'Template literal test: ' + \`Count is \${count}\`"></div>
|
|
21
|
+
<div @text="'Complex expr: ' + (count > 5 ? 'High' : 'Low')"></div>
|
|
22
|
+
<button @click="calculate()">Test Calculation</button>
|
|
23
|
+
<div @text="calcResult"></div>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<script type="module">
|
|
27
|
+
import helium from './helium-csp.js';
|
|
28
|
+
|
|
29
|
+
// Test that CSP-safe reactivity works
|
|
30
|
+
const state = await helium({
|
|
31
|
+
message: 'Hello from helium-csp!',
|
|
32
|
+
count: 0,
|
|
33
|
+
timer: 0,
|
|
34
|
+
calcResult: 'Not calculated'
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
state.calculate = function() {
|
|
38
|
+
// Test that calculations work without new Function
|
|
39
|
+
this.calcResult = `Square of ${this.count} = ${this.count * this.count}`;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Test timer to verify reactivity
|
|
43
|
+
setInterval(() => {
|
|
44
|
+
state.timer = new Date().getSeconds();
|
|
45
|
+
}, 1000);
|
|
46
|
+
|
|
47
|
+
console.log('Helium CSP loaded successfully!', state);
|
|
48
|
+
</script>
|
|
49
|
+
</body>
|
|
50
|
+
</html>
|