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.
@@ -4,58 +4,58 @@
4
4
  children: [
5
5
  // Math
6
6
  { div: { id: math, children: [
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)] } }
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: ", $&&(true, true)] } },
16
- { span: { class: or, children: ["or: ", $||(false, true)] } },
17
- { span: { class: not, children: ["not: ", $!(false)] } }
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: ", $upper('hello')] } },
23
- { span: { class: lower, children: ["lower: ", $lower('HELLO')] } },
24
- { span: { class: concat, children: ["concat: ", $concat('Hello', ' ', 'World')] } }
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: ", $==(5, 5)] } },
30
- { span: { class: gt, children: ["gt: ", $>(10, 5)] } },
31
- { span: { class: lt, children: ["lt: ", $<(5, 10)] } }
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: ", $if(true, 'Yes', 'No')] } },
37
- { span: { class: if-false, children: ["if-false: ", $if(false, 'Yes', 'No')] } }
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: ", $join(/data/list, '-')] } },
43
- { span: { class: len, children: ["len: ", $len(/data/list)] } },
44
- { span: { class: first, children: ["first: ", $first(/data/list)] } }
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: ", $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...)] } }
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: ", $currency(1234.56, 'USD')] } },
58
- { span: { class: percent, children: ["percent: ", $percent(0.125)] } }
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 -> children -> [ { div: { id: 'math', children: [ { span: ... } ] } } ]
66
- const root = hydrated.div;
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.div?.id === id);
69
- return section.div.children;
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].span.children[1].value).toBe(15);
76
- expect(math[1].span.children[1].value).toBe(5);
77
- expect(math[2].span.children[1].value).toBe(50);
78
- expect(math[3].span.children[1].value).toBe(2);
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].span.children[1].value).toBe(true);
83
- expect(logic[1].span.children[1].value).toBe(true);
84
- expect(logic[2].span.children[1].value).toBe(true);
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].span.children[1].value).toBe('HELLO');
89
- expect(str[1].span.children[1].value).toBe('hello');
90
- expect(str[2].span.children[1].value).toBe('Hello World');
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].span.children[1].value).toBe(true);
95
- expect(compare[1].span.children[1].value).toBe(true);
96
- expect(compare[2].span.children[1].value).toBe(true);
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].span.children[1].value).toBe('Yes');
101
- expect(conditional[1].span.children[1].value).toBe('No');
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].span.children[1].value).toBe('a-b-c');
106
- expect(array[1].span.children[1].value).toBe(3);
107
- expect(array[2].span.children[1].value).toBe('a');
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].span.children[1].value).toBe(100);
112
- expect(stats[1].span.children[1].value).toBe(25);
113
- expect(stats[2].span.children[1].value).toBe(40);
114
- expect(stats[3].span.children[1].value).toBe(10);
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].span.children[1].value).toBe('USD1,234.56');
119
- expect(format[1].span.children[1].value).toBe('13%');
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 { resolvePath, parseExpression, registerHelper, parseCDOMC } from '../../jprx/parser.js';
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
- describe('cdom Parser', () => {
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 $ expressions as strings', () => {
112
- const input = '{ button: { onclick: $increment($/count), children: "Click" } }';
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: '$increment($/count)',
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 $ prefix in simple paths', () => {
136
- const input = '{ input: { cdom-bind: $/user/name } }';
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': '$/user/name'
54
+ 'cdom-bind': '=/user/name'
142
55
  }
143
56
  });
144
57
  });
145
58
  });
146
59
 
147
60
  describe('Hydration', () => {
148
- it('converts event handler $ expressions to functions', () => {
149
-
61
+ it('converts event handler = expressions to functions', () => {
150
62
  const input = {
151
63
  button: {
152
- onclick: '$increment($/count)',
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.button.onclick).toBe('function');
161
- expect(result.button.children).toEqual(['Click']);
72
+ expect(typeof result.attributes.onclick).toBe('function');
73
+ expect(result.children).toEqual(['Click']);
162
74
  });
163
75
 
164
- it('preserves non-$ event handlers as strings', () => {
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-$ expression)
176
- expect(result.button.onclick).toBe('alert("hello")');
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('$/pageTitle');
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
- const totalExpr = LightviewCDOM.parseExpression('$/sum(bills/amount...)');
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
- const greeting = LightviewCDOM.parseExpression('$/user/if(loggedIn, concat("Welcome, ", ./name), "Please Login")');
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('$/settings/theme/colors/primary');
72
- const sizeExpr = LightviewCDOM.parseExpression('$/settings/theme/font/size');
73
- const deepExpr = LightviewCDOM.parseExpression('$/concat(settings/theme/colors/primary, "-", settings/theme/font/size)');
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: '$/profile/ui/theme', // Bound to ui.theme
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: ['$/profile/user/details/name'] } }, // Bound to user.details.name
125
- { img: { src: '$/profile/user/details/avatar/src', alt: '$/profile/user/details/avatar/alt' } }
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: ", '$/profile/user/stats/posts'] } }, // Bound to user.stats.posts
134
- { span: { children: ["Followers: ", '$/profile/user/stats/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
- // Logic + String helpers: "HELLO BOB" or "HELLO USER"
144
- { p: { children: ['$/upper(if(profile/user/details/name, profile/user/details/name, "User"))'] } },
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: ', '$/sum(profile/user/stats/posts, profile/user/stats/followers)'] } }
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.div;
159
- const header = root.children[0].div;
160
- const stats = root.children[1].div;
161
- const helpers = root.children[2].div;
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].h1.children[0].value).toBe('Bob');
165
- expect(header.children[1].img.src.value).toBe('img.png');
166
- expect(stats.children[0].span.children[1].value).toBe(10);
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].p.children[0].value).toBe('BOB'); // upper('Bob')
170
- expect(helpers.children[1].p.children[1].value).toBe(60); // sum(10, 50)
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].h1.children[0].value).toBe('Robert'); // Deep nested text
180
- expect(stats.children[0].span.children[1].value).toBe(20); // Deep nested sibling
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].p.children[0].value).toBe('ROBERT'); // upper('Robert')
184
- expect(helpers.children[1].p.children[1].value).toBe(70); // sum(20, 50)
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
+ });