lightview 2.2.2 → 2.3.4
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/cDOMIntro.md +279 -0
- package/docs/about.html +15 -12
- package/docs/api/computed.html +1 -1
- package/docs/api/effects.html +1 -1
- package/docs/api/elements.html +56 -25
- package/docs/api/enhance.html +1 -1
- package/docs/api/hypermedia.html +1 -1
- package/docs/api/index.html +1 -1
- package/docs/api/nav.html +28 -3
- package/docs/api/signals.html +1 -1
- package/docs/api/state.html +283 -85
- package/docs/assets/js/examplify.js +2 -1
- package/docs/cdom-nav.html +3 -2
- package/docs/cdom.html +383 -114
- package/jprx/README.md +112 -71
- package/jprx/helpers/state.js +21 -0
- package/jprx/package.json +1 -1
- package/jprx/parser.js +136 -86
- package/jprx/specs/expressions.json +71 -0
- package/jprx/specs/helpers.json +150 -0
- package/lightview-all.js +618 -431
- package/lightview-cdom.js +311 -605
- package/lightview-router.js +6 -0
- package/lightview-x.js +226 -54
- package/lightview.js +351 -42
- package/package.json +2 -1
- package/src/lightview-cdom.js +211 -315
- package/src/lightview-router.js +10 -0
- package/src/lightview-x.js +121 -1
- package/src/lightview.js +88 -16
- package/src/reactivity/signal.js +73 -29
- package/src/reactivity/state.js +84 -21
- package/tests/cdom/fixtures/helpers.cdomc +24 -24
- package/tests/cdom/helpers.test.js +28 -28
- package/tests/cdom/parser.test.js +39 -114
- package/tests/cdom/reactivity.test.js +32 -29
- package/tests/jprx/spec.test.js +99 -0
- package/tests/cdom/loader.test.js +0 -125
|
@@ -4,58 +4,58 @@
|
|
|
4
4
|
children: [
|
|
5
5
|
// Math
|
|
6
6
|
{ div: { id: math, children: [
|
|
7
|
-
{ span: { class: add, children: ["add: ",
|
|
8
|
-
{ span: { class: sub, children: ["sub: ",
|
|
9
|
-
{ span: { class: mul, children: ["mul: ",
|
|
10
|
-
{ span: { class: div, children: ["div: ",
|
|
7
|
+
{ span: { class: add, children: ["add: ", =+(10, 5)] } },
|
|
8
|
+
{ span: { class: sub, children: ["sub: ", =-(10, 5)] } },
|
|
9
|
+
{ span: { class: mul, children: ["mul: ", =*(10, 5)] } },
|
|
10
|
+
{ span: { class: div, children: ["div: ", =/(10, 5)] } }
|
|
11
11
|
]}},
|
|
12
12
|
|
|
13
13
|
// Logic
|
|
14
14
|
{ div: { id: logic, children: [
|
|
15
|
-
{ span: { class: and, children: ["and: ",
|
|
16
|
-
{ span: { class: or, children: ["or: ",
|
|
17
|
-
{ span: { class: not, children: ["not: ",
|
|
15
|
+
{ span: { class: and, children: ["and: ", =&&(true, true)] } },
|
|
16
|
+
{ span: { class: or, children: ["or: ", =||(false, true)] } },
|
|
17
|
+
{ span: { class: not, children: ["not: ", =!(false)] } }
|
|
18
18
|
]}},
|
|
19
19
|
|
|
20
20
|
// String
|
|
21
21
|
{ div: { id: string, children: [
|
|
22
|
-
{ span: { class: upper, children: ["upper: ",
|
|
23
|
-
{ span: { class: lower, children: ["lower: ",
|
|
24
|
-
{ span: { class: concat, children: ["concat: ",
|
|
22
|
+
{ span: { class: upper, children: ["upper: ", =upper('hello')] } },
|
|
23
|
+
{ span: { class: lower, children: ["lower: ", =lower('HELLO')] } },
|
|
24
|
+
{ span: { class: concat, children: ["concat: ", =concat('Hello', ' ', 'World')] } }
|
|
25
25
|
]}},
|
|
26
26
|
|
|
27
27
|
// Compare
|
|
28
28
|
{ div: { id: compare, children: [
|
|
29
|
-
{ span: { class: eq, children: ["eq: ",
|
|
30
|
-
{ span: { class: gt, children: ["gt: ",
|
|
31
|
-
{ span: { class: lt, children: ["lt: ",
|
|
29
|
+
{ span: { class: eq, children: ["eq: ", ===(5, 5)] } },
|
|
30
|
+
{ span: { class: gt, children: ["gt: ", =>(10, 5)] } },
|
|
31
|
+
{ span: { class: lt, children: ["lt: ", =<(5, 10)] } }
|
|
32
32
|
]}},
|
|
33
33
|
|
|
34
34
|
// Conditional
|
|
35
35
|
{ div: { id: conditional, children: [
|
|
36
|
-
{ span: { class: if-true, children: ["if-true: ",
|
|
37
|
-
{ span: { class: if-false, children: ["if-false: ",
|
|
36
|
+
{ span: { class: if-true, children: ["if-true: ", =if(true, 'Yes', 'No')] } },
|
|
37
|
+
{ span: { class: if-false, children: ["if-false: ", =if(false, 'Yes', 'No')] } }
|
|
38
38
|
]}},
|
|
39
39
|
|
|
40
40
|
// Array (using global state data)
|
|
41
41
|
{ div: { id: array, children: [
|
|
42
|
-
{ span: { class: join, children: ["join: ",
|
|
43
|
-
{ span: { class: len, children: ["len: ",
|
|
44
|
-
{ span: { class: first, children: ["first: ",
|
|
42
|
+
{ span: { class: join, children: ["join: ", =join(/data/list, '-')] } },
|
|
43
|
+
{ span: { class: len, children: ["len: ", =len(/data/list)] } },
|
|
44
|
+
{ span: { class: first, children: ["first: ", =first(/data/list)] } }
|
|
45
45
|
]}},
|
|
46
46
|
|
|
47
47
|
// Stats
|
|
48
48
|
{ div: { id: stats, children: [
|
|
49
|
-
{ span: { class: sum, children: ["sum: ",
|
|
50
|
-
{ span: { class: avg, children: ["avg: ",
|
|
51
|
-
{ span: { class: max, children: ["max: ",
|
|
52
|
-
{ span: { class: min, children: ["min: ",
|
|
49
|
+
{ span: { class: sum, children: ["sum: ", =sum(/data/numbers...)] } },
|
|
50
|
+
{ span: { class: avg, children: ["avg: ", =avg(/data/numbers...)] } },
|
|
51
|
+
{ span: { class: max, children: ["max: ", =max(/data/numbers...)] } },
|
|
52
|
+
{ span: { class: min, children: ["min: ", =min(/data/numbers...)] } }
|
|
53
53
|
]}},
|
|
54
54
|
|
|
55
55
|
// Format
|
|
56
56
|
{ div: { id: format, children: [
|
|
57
|
-
{ span: { class: currency, children: ["currency: ",
|
|
58
|
-
{ span: { class: percent, children: ["percent: ",
|
|
57
|
+
{ span: { class: currency, children: ["currency: ", =currency(1234.56, 'USD')] } },
|
|
58
|
+
{ span: { class: percent, children: ["percent: ", =percent(0.125)] } }
|
|
59
59
|
]}}
|
|
60
60
|
]
|
|
61
61
|
}
|
|
@@ -62,60 +62,60 @@ describe('cdom Helpers Integration', () => {
|
|
|
62
62
|
const hydrated = LightviewCDOM.hydrate(parsed);
|
|
63
63
|
|
|
64
64
|
// Helper to dive into the structure:
|
|
65
|
-
// div
|
|
66
|
-
const root = hydrated
|
|
65
|
+
// { tag: 'div', children: [ { tag: 'div', attributes: { id: 'math' }, children: [...] } ] }
|
|
66
|
+
const root = hydrated;
|
|
67
67
|
const findSection = (id) => {
|
|
68
|
-
const section = root.children.find(child => child.
|
|
69
|
-
return section.
|
|
68
|
+
const section = root.children.find(child => child.attributes?.id === id);
|
|
69
|
+
return section.children;
|
|
70
70
|
};
|
|
71
71
|
|
|
72
72
|
// Math
|
|
73
73
|
const math = findSection('math');
|
|
74
74
|
// add: $+(10, 5) -> "add: " + 15
|
|
75
|
-
expect(math[0].
|
|
76
|
-
expect(math[1].
|
|
77
|
-
expect(math[2].
|
|
78
|
-
expect(math[3].
|
|
75
|
+
expect(math[0].children[1].value).toBe(15);
|
|
76
|
+
expect(math[1].children[1].value).toBe(5);
|
|
77
|
+
expect(math[2].children[1].value).toBe(50);
|
|
78
|
+
expect(math[3].children[1].value).toBe(2);
|
|
79
79
|
|
|
80
80
|
// Logic
|
|
81
81
|
const logic = findSection('logic');
|
|
82
|
-
expect(logic[0].
|
|
83
|
-
expect(logic[1].
|
|
84
|
-
expect(logic[2].
|
|
82
|
+
expect(logic[0].children[1].value).toBe(true);
|
|
83
|
+
expect(logic[1].children[1].value).toBe(true);
|
|
84
|
+
expect(logic[2].children[1].value).toBe(true);
|
|
85
85
|
|
|
86
86
|
// String
|
|
87
87
|
const str = findSection('string');
|
|
88
|
-
expect(str[0].
|
|
89
|
-
expect(str[1].
|
|
90
|
-
expect(str[2].
|
|
88
|
+
expect(str[0].children[1].value).toBe('HELLO');
|
|
89
|
+
expect(str[1].children[1].value).toBe('hello');
|
|
90
|
+
expect(str[2].children[1].value).toBe('Hello World');
|
|
91
91
|
|
|
92
92
|
// Compare
|
|
93
93
|
const compare = findSection('compare');
|
|
94
|
-
expect(compare[0].
|
|
95
|
-
expect(compare[1].
|
|
96
|
-
expect(compare[2].
|
|
94
|
+
expect(compare[0].children[1].value).toBe(true);
|
|
95
|
+
expect(compare[1].children[1].value).toBe(true);
|
|
96
|
+
expect(compare[2].children[1].value).toBe(true);
|
|
97
97
|
|
|
98
98
|
// Conditional
|
|
99
99
|
const conditional = findSection('conditional');
|
|
100
|
-
expect(conditional[0].
|
|
101
|
-
expect(conditional[1].
|
|
100
|
+
expect(conditional[0].children[1].value).toBe('Yes');
|
|
101
|
+
expect(conditional[1].children[1].value).toBe('No');
|
|
102
102
|
|
|
103
103
|
// Array
|
|
104
104
|
const array = findSection('array');
|
|
105
|
-
expect(array[0].
|
|
106
|
-
expect(array[1].
|
|
107
|
-
expect(array[2].
|
|
105
|
+
expect(array[0].children[1].value).toBe('a-b-c');
|
|
106
|
+
expect(array[1].children[1].value).toBe(3);
|
|
107
|
+
expect(array[2].children[1].value).toBe('a');
|
|
108
108
|
|
|
109
109
|
// Stats
|
|
110
110
|
const stats = findSection('stats');
|
|
111
|
-
expect(stats[0].
|
|
112
|
-
expect(stats[1].
|
|
113
|
-
expect(stats[2].
|
|
114
|
-
expect(stats[3].
|
|
111
|
+
expect(stats[0].children[1].value).toBe(100);
|
|
112
|
+
expect(stats[1].children[1].value).toBe(25);
|
|
113
|
+
expect(stats[2].children[1].value).toBe(40);
|
|
114
|
+
expect(stats[3].children[1].value).toBe(10);
|
|
115
115
|
|
|
116
116
|
// Format
|
|
117
117
|
const format = findSection('format');
|
|
118
|
-
expect(format[0].
|
|
119
|
-
expect(format[1].
|
|
118
|
+
expect(format[0].children[1].value).toBe('USD1,234.56');
|
|
119
|
+
expect(format[1].children[1].value).toBe('13%');
|
|
120
120
|
});
|
|
121
121
|
});
|
|
@@ -1,120 +1,33 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
2
|
import Lightview from '../../src/lightview.js';
|
|
3
3
|
import LightviewX from '../../src/lightview-x.js';
|
|
4
|
-
import {
|
|
5
|
-
import { registerMathHelpers } from '../../jprx/helpers/math.js';
|
|
6
|
-
import { registerLogicHelpers } from '../../jprx/helpers/logic.js';
|
|
7
|
-
import { registerStringHelpers } from '../../jprx/helpers/string.js';
|
|
8
|
-
import { registerArrayHelpers } from '../../jprx/helpers/array.js';
|
|
9
|
-
import { registerCompareHelpers } from '../../jprx/helpers/compare.js';
|
|
10
|
-
import { registerConditionalHelpers } from '../../jprx/helpers/conditional.js';
|
|
11
|
-
import { registerDateTimeHelpers } from '../../jprx/helpers/datetime.js';
|
|
12
|
-
import { registerFormatHelpers } from '../../jprx/helpers/format.js';
|
|
13
|
-
import { registerLookupHelpers } from '../../jprx/helpers/lookup.js';
|
|
14
|
-
import { registerStatsHelpers } from '../../jprx/helpers/stats.js';
|
|
15
|
-
import { registerStateHelpers } from '../../jprx/helpers/state.js';
|
|
4
|
+
import { parseCDOMC } from '../../jprx/parser.js';
|
|
16
5
|
import { hydrate } from '../../src/lightview-cdom.js';
|
|
17
6
|
|
|
18
|
-
|
|
7
|
+
/**
|
|
8
|
+
* cdom Integration Tests
|
|
9
|
+
*
|
|
10
|
+
* These tests focus on Lightview-specific functionality:
|
|
11
|
+
* - CDOMC parsing (concise syntax)
|
|
12
|
+
* - Hydration (converting static objects to reactive structures)
|
|
13
|
+
* - Event handler conversion
|
|
14
|
+
*
|
|
15
|
+
* Pure JPRX expression tests are in tests/jprx/spec.test.js
|
|
16
|
+
*/
|
|
17
|
+
describe('cdom Integration', () => {
|
|
19
18
|
beforeEach(() => {
|
|
20
|
-
// Clear registry before each test
|
|
21
19
|
Lightview.registry.clear();
|
|
22
|
-
// Register standard helpers
|
|
23
|
-
registerMathHelpers(registerHelper);
|
|
24
|
-
registerLogicHelpers(registerHelper);
|
|
25
|
-
registerStringHelpers(registerHelper);
|
|
26
|
-
registerArrayHelpers(registerHelper);
|
|
27
|
-
registerCompareHelpers(registerHelper);
|
|
28
|
-
registerConditionalHelpers(registerHelper);
|
|
29
|
-
registerDateTimeHelpers(registerHelper);
|
|
30
|
-
registerFormatHelpers(registerHelper);
|
|
31
|
-
registerLookupHelpers(registerHelper);
|
|
32
|
-
registerStatsHelpers(registerHelper);
|
|
33
|
-
registerStateHelpers((name, fn) => registerHelper(name, fn, { pathAware: true }));
|
|
34
|
-
|
|
35
|
-
// Attach to global for the parser to find it
|
|
36
20
|
globalThis.Lightview = Lightview;
|
|
37
21
|
});
|
|
38
22
|
|
|
39
|
-
describe('Path Resolution', () => {
|
|
40
|
-
it('resolves absolute global paths', () => {
|
|
41
|
-
Lightview.signal('Alice', 'userName');
|
|
42
|
-
expect(resolvePath('$/userName')).toBe('Alice');
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('resolves deep global paths in state', () => {
|
|
46
|
-
LightviewX.state({ profile: { name: 'Bob' } }, 'user');
|
|
47
|
-
expect(resolvePath('$/user/profile/name')).toBe('Bob');
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('resolves relative paths against context', () => {
|
|
51
|
-
const context = { age: 30, city: 'London' };
|
|
52
|
-
expect(resolvePath('./age', context)).toBe(30);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('resolves array indices', () => {
|
|
56
|
-
const context = { items: ['apple', 'banana'] };
|
|
57
|
-
expect(resolvePath('./items/1', context)).toBe('banana');
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
describe('Expression Parsing', () => {
|
|
62
|
-
it('creates a computed signal for a path', () => {
|
|
63
|
-
const sig = Lightview.signal(10, 'count');
|
|
64
|
-
const expr = parseExpression('$/count');
|
|
65
|
-
|
|
66
|
-
expect(expr.value).toBe(10);
|
|
67
|
-
|
|
68
|
-
sig.value = 20;
|
|
69
|
-
expect(expr.value).toBe(20);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('supports math helpers', () => {
|
|
73
|
-
Lightview.signal(5, 'a');
|
|
74
|
-
Lightview.signal(10, 'b');
|
|
75
|
-
const expr = parseExpression('$/+(a, b)');
|
|
76
|
-
|
|
77
|
-
expect(expr.value).toBe(15);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('supports logical helpers', () => {
|
|
81
|
-
Lightview.signal(true, 'isVip');
|
|
82
|
-
const expr = parseExpression('$/if(isVip, "Gold", "Silver")');
|
|
83
|
-
|
|
84
|
-
expect(expr.value).toBe('Gold');
|
|
85
|
-
|
|
86
|
-
Lightview.get('isVip').value = false;
|
|
87
|
-
expect(expr.value).toBe('Silver');
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('supports the explosion operator (...)', () => {
|
|
91
|
-
LightviewX.state({ scores: [10, 20, 30] }, 'game');
|
|
92
|
-
const expr = parseExpression('$/sum(game/scores...)');
|
|
93
|
-
|
|
94
|
-
expect(expr.value).toBe(60);
|
|
95
|
-
|
|
96
|
-
const scores = Lightview.get('game').scores;
|
|
97
|
-
scores.push(40);
|
|
98
|
-
expect(expr.value).toBe(100);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('handles nested navigation with helpers ($/user/upper(name))', () => {
|
|
102
|
-
LightviewX.state({ user: { name: 'charlie' } }, 'app');
|
|
103
|
-
// This tests: navigate to app/user, then call upper() on app/user.name
|
|
104
|
-
const expr = parseExpression('$/app/user/upper(name)');
|
|
105
|
-
|
|
106
|
-
expect(expr.value).toBe('CHARLIE');
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
|
-
|
|
110
23
|
describe('CDOMC Parser', () => {
|
|
111
|
-
it('parses unquoted
|
|
112
|
-
const input = '{ button: { onclick:
|
|
24
|
+
it('parses unquoted = expressions as strings', () => {
|
|
25
|
+
const input = '{ button: { onclick: =increment(=/count), children: "Click" } }';
|
|
113
26
|
const result = parseCDOMC(input);
|
|
114
27
|
|
|
115
28
|
expect(result).toEqual({
|
|
116
29
|
button: {
|
|
117
|
-
onclick: '
|
|
30
|
+
onclick: '=increment(=/count)',
|
|
118
31
|
children: 'Click'
|
|
119
32
|
}
|
|
120
33
|
});
|
|
@@ -132,24 +45,23 @@ describe('cdom Parser', () => {
|
|
|
132
45
|
});
|
|
133
46
|
});
|
|
134
47
|
|
|
135
|
-
it('preserves
|
|
136
|
-
const input = '{ input: { cdom-bind:
|
|
48
|
+
it('preserves = prefix in simple paths', () => {
|
|
49
|
+
const input = '{ input: { cdom-bind: =/user/name } }';
|
|
137
50
|
const result = parseCDOMC(input);
|
|
138
51
|
|
|
139
52
|
expect(result).toEqual({
|
|
140
53
|
input: {
|
|
141
|
-
'cdom-bind': '
|
|
54
|
+
'cdom-bind': '=/user/name'
|
|
142
55
|
}
|
|
143
56
|
});
|
|
144
57
|
});
|
|
145
58
|
});
|
|
146
59
|
|
|
147
60
|
describe('Hydration', () => {
|
|
148
|
-
it('converts event handler
|
|
149
|
-
|
|
61
|
+
it('converts event handler = expressions to functions', () => {
|
|
150
62
|
const input = {
|
|
151
63
|
button: {
|
|
152
|
-
onclick: '
|
|
64
|
+
onclick: '=increment(=/count)',
|
|
153
65
|
children: ['Click']
|
|
154
66
|
}
|
|
155
67
|
};
|
|
@@ -157,12 +69,11 @@ describe('cdom Parser', () => {
|
|
|
157
69
|
const result = hydrate(input);
|
|
158
70
|
|
|
159
71
|
// onclick should now be a function
|
|
160
|
-
expect(typeof result.
|
|
161
|
-
expect(result.
|
|
72
|
+
expect(typeof result.attributes.onclick).toBe('function');
|
|
73
|
+
expect(result.children).toEqual(['Click']);
|
|
162
74
|
});
|
|
163
75
|
|
|
164
|
-
it('preserves non
|
|
165
|
-
|
|
76
|
+
it('preserves non-= event handlers as strings', () => {
|
|
166
77
|
const input = {
|
|
167
78
|
button: {
|
|
168
79
|
onclick: 'alert("hello")',
|
|
@@ -172,8 +83,22 @@ describe('cdom Parser', () => {
|
|
|
172
83
|
|
|
173
84
|
const result = hydrate(input);
|
|
174
85
|
|
|
175
|
-
// onclick should remain a string (non
|
|
176
|
-
expect(result.
|
|
86
|
+
// onclick should remain a string (non-= expression)
|
|
87
|
+
expect(result.attributes.onclick).toBe('alert("hello")');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('handles escape sequence for literal = strings', () => {
|
|
91
|
+
// Test escape in children array context
|
|
92
|
+
const input = {
|
|
93
|
+
div: {
|
|
94
|
+
children: ["'=E=mc²"]
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const result = hydrate(input);
|
|
99
|
+
|
|
100
|
+
// Should strip the leading ' and keep the rest as literal
|
|
101
|
+
expect(result.children[0]).toBe('=E=mc²');
|
|
177
102
|
});
|
|
178
103
|
});
|
|
179
104
|
});
|
|
@@ -12,7 +12,7 @@ describe('cdom Reactivity', () => {
|
|
|
12
12
|
|
|
13
13
|
it('should update DOM-like values when signals change', () => {
|
|
14
14
|
const title = Lightview.signal('Hello', 'pageTitle');
|
|
15
|
-
const expr = LightviewCDOM.parseExpression('
|
|
15
|
+
const expr = LightviewCDOM.parseExpression('=/pageTitle');
|
|
16
16
|
|
|
17
17
|
expect(expr.value).toBe('Hello');
|
|
18
18
|
|
|
@@ -26,7 +26,8 @@ describe('cdom Reactivity', () => {
|
|
|
26
26
|
{ id: 2, amount: 200 }
|
|
27
27
|
], 'bills');
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
// Correct syntax: =sum() is the function, /bills...amount is the argument
|
|
30
|
+
const totalExpr = LightviewCDOM.parseExpression('=sum(/bills...amount)');
|
|
30
31
|
expect(totalExpr.value).toBe(300);
|
|
31
32
|
|
|
32
33
|
// Update an existing item
|
|
@@ -44,7 +45,8 @@ describe('cdom Reactivity', () => {
|
|
|
44
45
|
|
|
45
46
|
it('should handle conditional logic reactively', () => {
|
|
46
47
|
const user = LightviewX.state({ loggedIn: false, name: 'Guest' }, 'user');
|
|
47
|
-
|
|
48
|
+
// Correct syntax: =if() is the function with paths as arguments
|
|
49
|
+
const greeting = LightviewCDOM.parseExpression('=if(/user/loggedIn, concat("Welcome, ", /user/name), "Please Login")');
|
|
48
50
|
|
|
49
51
|
expect(greeting.value).toBe('Please Login');
|
|
50
52
|
|
|
@@ -52,6 +54,7 @@ describe('cdom Reactivity', () => {
|
|
|
52
54
|
user.name = 'Alice';
|
|
53
55
|
expect(greeting.value).toBe('Welcome, Alice');
|
|
54
56
|
});
|
|
57
|
+
|
|
55
58
|
it('should handle reactivity on multi-level nested objects', () => {
|
|
56
59
|
const settings = LightviewX.state({
|
|
57
60
|
theme: {
|
|
@@ -68,9 +71,9 @@ describe('cdom Reactivity', () => {
|
|
|
68
71
|
}
|
|
69
72
|
}, 'settings');
|
|
70
73
|
|
|
71
|
-
const colorExpr = LightviewCDOM.parseExpression('
|
|
72
|
-
const sizeExpr = LightviewCDOM.parseExpression('
|
|
73
|
-
const deepExpr = LightviewCDOM.parseExpression('
|
|
74
|
+
const colorExpr = LightviewCDOM.parseExpression('=/settings/theme/colors/primary');
|
|
75
|
+
const sizeExpr = LightviewCDOM.parseExpression('=/settings/theme/font/size');
|
|
76
|
+
const deepExpr = LightviewCDOM.parseExpression('=concat(/settings/theme/colors/primary, "-", /settings/theme/font/size)');
|
|
74
77
|
|
|
75
78
|
expect(colorExpr.value).toBe('blue');
|
|
76
79
|
expect(sizeExpr.value).toBe('16px');
|
|
@@ -115,14 +118,14 @@ describe('cdom Reactivity', () => {
|
|
|
115
118
|
const cdomcStructure = {
|
|
116
119
|
div: {
|
|
117
120
|
id: 'profile-card',
|
|
118
|
-
class: '
|
|
121
|
+
class: '=/profile/ui/theme', // Bound to ui.theme
|
|
119
122
|
children: [
|
|
120
123
|
{
|
|
121
124
|
div: {
|
|
122
125
|
class: 'header',
|
|
123
126
|
children: [
|
|
124
|
-
{ h1: { children: ['
|
|
125
|
-
{ img: { src: '
|
|
127
|
+
{ h1: { children: ['=/profile/user/details/name'] } }, // Bound to user.details.name
|
|
128
|
+
{ img: { src: '=/profile/user/details/avatar/src', alt: '=/profile/user/details/avatar/alt' } }
|
|
126
129
|
]
|
|
127
130
|
}
|
|
128
131
|
},
|
|
@@ -130,8 +133,8 @@ describe('cdom Reactivity', () => {
|
|
|
130
133
|
div: {
|
|
131
134
|
class: 'stats',
|
|
132
135
|
children: [
|
|
133
|
-
{ span: { children: ["Posts: ", '
|
|
134
|
-
{ span: { children: ["Followers: ", '
|
|
136
|
+
{ span: { children: ["Posts: ", '=/profile/user/stats/posts'] } }, // Bound to user.stats.posts
|
|
137
|
+
{ span: { children: ["Followers: ", '=/profile/user/stats/followers'] } }
|
|
135
138
|
]
|
|
136
139
|
}
|
|
137
140
|
},
|
|
@@ -140,10 +143,10 @@ describe('cdom Reactivity', () => {
|
|
|
140
143
|
div: {
|
|
141
144
|
class: 'helpers-test',
|
|
142
145
|
children: [
|
|
143
|
-
//
|
|
144
|
-
{ p: { children: ['
|
|
146
|
+
// Correct syntax: =upper() is the function, with nested if() and path
|
|
147
|
+
{ p: { children: ['=upper(if(/profile/user/details/name, /profile/user/details/name, "User"))'] } },
|
|
145
148
|
// Math + Stats helpers: Sum of posts and followers
|
|
146
|
-
{ p: { children: ['Total interactions: ', '
|
|
149
|
+
{ p: { children: ['Total interactions: ', '=sum(/profile/user/stats/posts, /profile/user/stats/followers)'] } }
|
|
147
150
|
]
|
|
148
151
|
}
|
|
149
152
|
}
|
|
@@ -155,19 +158,19 @@ describe('cdom Reactivity', () => {
|
|
|
155
158
|
const hydrated = LightviewCDOM.hydrate(cdomcStructure);
|
|
156
159
|
|
|
157
160
|
// 4. Inspect & Assert Initial State
|
|
158
|
-
const root = hydrated
|
|
159
|
-
const header = root.children[0]
|
|
160
|
-
const stats = root.children[1]
|
|
161
|
-
const helpers = root.children[2]
|
|
161
|
+
const root = hydrated;
|
|
162
|
+
const header = root.children[0];
|
|
163
|
+
const stats = root.children[1];
|
|
164
|
+
const helpers = root.children[2];
|
|
162
165
|
|
|
163
|
-
expect(root.class.value).toBe('dark');
|
|
164
|
-
expect(header.children[0].
|
|
165
|
-
expect(header.children[1].
|
|
166
|
-
expect(stats.children[0].
|
|
166
|
+
expect(root.attributes.class.value).toBe('dark');
|
|
167
|
+
expect(header.children[0].children[0].value).toBe('Bob');
|
|
168
|
+
expect(header.children[1].attributes.src.value).toBe('img.png');
|
|
169
|
+
expect(stats.children[0].children[1].value).toBe(10);
|
|
167
170
|
|
|
168
171
|
// Assert Helpers Initial State
|
|
169
|
-
expect(helpers.children[0].
|
|
170
|
-
expect(helpers.children[1].
|
|
172
|
+
expect(helpers.children[0].children[0].value).toBe('BOB'); // upper('Bob')
|
|
173
|
+
expect(helpers.children[1].children[1].value).toBe(60); // sum(10, 50)
|
|
171
174
|
|
|
172
175
|
// 5. Trigger Deep Updates
|
|
173
176
|
profile.user.details.name = 'Robert';
|
|
@@ -175,12 +178,12 @@ describe('cdom Reactivity', () => {
|
|
|
175
178
|
profile.user.stats.posts = 20;
|
|
176
179
|
|
|
177
180
|
// 6. Assert Updates Propagated to cdomC Structure
|
|
178
|
-
expect(root.class.value).toBe('light'); // Top-level attr
|
|
179
|
-
expect(header.children[0].
|
|
180
|
-
expect(stats.children[0].
|
|
181
|
+
expect(root.attributes.class.value).toBe('light'); // Top-level attr
|
|
182
|
+
expect(header.children[0].children[0].value).toBe('Robert'); // Deep nested text
|
|
183
|
+
expect(stats.children[0].children[1].value).toBe(20); // Deep nested sibling
|
|
181
184
|
|
|
182
185
|
// Assert Helpers Updated State
|
|
183
|
-
expect(helpers.children[0].
|
|
184
|
-
expect(helpers.children[1].
|
|
186
|
+
expect(helpers.children[0].children[0].value).toBe('ROBERT'); // upper('Robert')
|
|
187
|
+
expect(helpers.children[1].children[1].value).toBe(70); // sum(20, 50)
|
|
185
188
|
});
|
|
186
189
|
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import Lightview from '../../src/lightview.js';
|
|
5
|
+
import LightviewX from '../../src/lightview-x.js';
|
|
6
|
+
import { resolveExpression, registerHelper, registerOperator } from '../../jprx/parser.js';
|
|
7
|
+
import { registerMathHelpers } from '../../jprx/helpers/math.js';
|
|
8
|
+
import { registerLogicHelpers } from '../../jprx/helpers/logic.js';
|
|
9
|
+
import { registerStatsHelpers } from '../../jprx/helpers/stats.js';
|
|
10
|
+
import { registerCompareHelpers } from '../../jprx/helpers/compare.js';
|
|
11
|
+
import { registerStringHelpers } from '../../jprx/helpers/string.js';
|
|
12
|
+
import { registerArrayHelpers } from '../../jprx/helpers/array.js';
|
|
13
|
+
import { registerConditionalHelpers } from '../../jprx/helpers/conditional.js';
|
|
14
|
+
import { registerDateTimeHelpers } from '../../jprx/helpers/datetime.js';
|
|
15
|
+
import { registerFormatHelpers } from '../../jprx/helpers/format.js';
|
|
16
|
+
import { registerLookupHelpers } from '../../jprx/helpers/lookup.js';
|
|
17
|
+
import { registerStateHelpers } from '../../jprx/helpers/state.js';
|
|
18
|
+
import { registerNetworkHelpers } from '../../jprx/helpers/network.js';
|
|
19
|
+
|
|
20
|
+
// Register standard operators
|
|
21
|
+
const registerStandardOperators = () => {
|
|
22
|
+
registerOperator('increment', '++', 'prefix', 80);
|
|
23
|
+
registerOperator('increment', '++', 'postfix', 80);
|
|
24
|
+
registerOperator('decrement', '--', 'prefix', 80);
|
|
25
|
+
registerOperator('decrement', '--', 'postfix', 80);
|
|
26
|
+
registerOperator('toggle', '!!', 'prefix', 80);
|
|
27
|
+
registerOperator('+', '+', 'infix', 50);
|
|
28
|
+
registerOperator('-', '-', 'infix', 50);
|
|
29
|
+
registerOperator('*', '*', 'infix', 60);
|
|
30
|
+
registerOperator('/', '/', 'infix', 60);
|
|
31
|
+
registerOperator('gt', '>', 'infix', 40);
|
|
32
|
+
registerOperator('lt', '<', 'infix', 40);
|
|
33
|
+
registerOperator('gte', '>=', 'infix', 40);
|
|
34
|
+
registerOperator('lte', '<=', 'infix', 40);
|
|
35
|
+
registerOperator('neq', '!=', 'infix', 40);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Helper to load and run specs
|
|
39
|
+
const runSpec = (specPath) => {
|
|
40
|
+
const specs = JSON.parse(fs.readFileSync(specPath, 'utf8'));
|
|
41
|
+
|
|
42
|
+
describe(path.basename(specPath), () => {
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
// Clear all global reactive state
|
|
45
|
+
Lightview.registry.clear();
|
|
46
|
+
Lightview.internals.futureSignals.clear();
|
|
47
|
+
|
|
48
|
+
registerMathHelpers(registerHelper);
|
|
49
|
+
registerLogicHelpers(registerHelper);
|
|
50
|
+
registerStatsHelpers(registerHelper);
|
|
51
|
+
registerCompareHelpers(registerHelper);
|
|
52
|
+
registerStringHelpers(registerHelper);
|
|
53
|
+
registerArrayHelpers(registerHelper);
|
|
54
|
+
registerConditionalHelpers(registerHelper);
|
|
55
|
+
registerDateTimeHelpers(registerHelper);
|
|
56
|
+
registerFormatHelpers(registerHelper);
|
|
57
|
+
registerLookupHelpers(registerHelper);
|
|
58
|
+
registerStateHelpers((name, fn) => registerHelper(name, fn, { pathAware: true }));
|
|
59
|
+
registerNetworkHelpers(registerHelper);
|
|
60
|
+
|
|
61
|
+
registerStandardOperators();
|
|
62
|
+
globalThis.Lightview = Lightview;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
specs.forEach((spec) => {
|
|
66
|
+
it(spec.name || spec.expression, () => {
|
|
67
|
+
// 1. Setup State
|
|
68
|
+
if (spec.state) {
|
|
69
|
+
Object.entries(spec.state).forEach(([key, value]) => {
|
|
70
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
71
|
+
LightviewX.state(value, key);
|
|
72
|
+
} else {
|
|
73
|
+
Lightview.signal(value, key);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 2. Evaluate Expression
|
|
79
|
+
const result = resolveExpression(spec.expression, spec.context);
|
|
80
|
+
|
|
81
|
+
// 3. Unwrap if it's a signal
|
|
82
|
+
const finalValue = (result && typeof result === 'function' && 'value' in result)
|
|
83
|
+
? result.value
|
|
84
|
+
: result;
|
|
85
|
+
|
|
86
|
+
expect(finalValue).toEqual(spec.expected);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
describe('JPRX Shared Specs', () => {
|
|
93
|
+
const specsDir = path.resolve(__dirname, '../../jprx/specs');
|
|
94
|
+
const files = fs.readdirSync(specsDir).filter(f => f.endsWith('.json'));
|
|
95
|
+
|
|
96
|
+
files.forEach(file => {
|
|
97
|
+
runSpec(path.join(specsDir, file));
|
|
98
|
+
});
|
|
99
|
+
});
|