leanweb 3.10.3 → 3.10.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/commands/build.js +3 -2
- package/package.json +7 -4
- package/test/lw-event-bus.test.js +104 -0
- package/test/lw-expr-parser.test.js +303 -0
- package/test/lw-html-parser.test.js +151 -0
- package/test/utils.test.js +94 -0
package/commands/build.js
CHANGED
|
@@ -48,9 +48,10 @@ const buildModule = (projectPath) => {
|
|
|
48
48
|
const globalCssPath = `${utils.dirs.build}/global-styles.css`;
|
|
49
49
|
const globalCss = fs.existsSync(globalCssPath) ? fs.readFileSync(globalCssPath, 'utf8') : '';
|
|
50
50
|
|
|
51
|
-
//
|
|
51
|
+
// Strip CSS comments, then extract @import statements and create a JS module that sets up the global styles
|
|
52
52
|
const imports = [];
|
|
53
|
-
const
|
|
53
|
+
const uncommented = globalCss.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
54
|
+
const inlineCss = uncommented.replace(/@import\s+(?:url\()?['"]?([^'"\)]+)['"]?\)?[^;]*;/g, (_, url) => {
|
|
54
55
|
imports.push(url);
|
|
55
56
|
return '';
|
|
56
57
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leanweb",
|
|
3
|
-
"version": "3.10.
|
|
3
|
+
"version": "3.10.4",
|
|
4
4
|
"description": "Builds framework agnostic web components.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"leanweb": "leanweb.js",
|
|
@@ -18,14 +18,17 @@
|
|
|
18
18
|
"web components"
|
|
19
19
|
],
|
|
20
20
|
"author": "Qian Chen",
|
|
21
|
+
"scripts": {
|
|
22
|
+
"test": "node --test test/*.test.js"
|
|
23
|
+
},
|
|
21
24
|
"license": "MIT",
|
|
22
25
|
"dependencies": {
|
|
23
|
-
"@babel/parser": "^7.29.
|
|
24
|
-
"esbuild": "^0.27.
|
|
26
|
+
"@babel/parser": "^7.29.2",
|
|
27
|
+
"esbuild": "^0.27.4",
|
|
25
28
|
"fs-extra": "^11.3.4",
|
|
26
29
|
"globby": "^16.1.1",
|
|
27
30
|
"html-minifier": "^4.0.0",
|
|
28
|
-
"isomorphic-git": "^1.37.
|
|
31
|
+
"isomorphic-git": "^1.37.4",
|
|
29
32
|
"live-server": "^1.2.2",
|
|
30
33
|
"node-watch": "^0.7.4",
|
|
31
34
|
"parse5": "^8.0.0",
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import LWEventBus from '../templates/lib/lw-event-bus.js';
|
|
4
|
+
|
|
5
|
+
describe('LWEventBus', () => {
|
|
6
|
+
it('should create an instance with empty listeners', () => {
|
|
7
|
+
const bus = new LWEventBus();
|
|
8
|
+
assert.deepEqual(bus.listeners, {});
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe('addEventListener', () => {
|
|
12
|
+
it('should register a listener and return it', () => {
|
|
13
|
+
const bus = new LWEventBus();
|
|
14
|
+
const callback = () => {};
|
|
15
|
+
const listener = bus.addEventListener('test', callback);
|
|
16
|
+
assert.equal(listener.eventName, 'test');
|
|
17
|
+
assert.equal(listener.callback, callback);
|
|
18
|
+
assert.ok(listener.key);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should register multiple listeners for same event', () => {
|
|
22
|
+
const bus = new LWEventBus();
|
|
23
|
+
bus.addEventListener('test', () => {});
|
|
24
|
+
bus.addEventListener('test', () => {});
|
|
25
|
+
assert.equal(Object.keys(bus.listeners['test']).length, 2);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should register listeners for different events', () => {
|
|
29
|
+
const bus = new LWEventBus();
|
|
30
|
+
bus.addEventListener('event1', () => {});
|
|
31
|
+
bus.addEventListener('event2', () => {});
|
|
32
|
+
assert.ok(bus.listeners['event1']);
|
|
33
|
+
assert.ok(bus.listeners['event2']);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('removeEventListener', () => {
|
|
38
|
+
it('should remove a registered listener', () => {
|
|
39
|
+
const bus = new LWEventBus();
|
|
40
|
+
const listener = bus.addEventListener('test', () => {});
|
|
41
|
+
bus.removeEventListener(listener);
|
|
42
|
+
assert.equal(Object.keys(bus.listeners['test']).length, 0);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should not throw when removing from non-existent event', () => {
|
|
46
|
+
const bus = new LWEventBus();
|
|
47
|
+
assert.doesNotThrow(() => {
|
|
48
|
+
bus.removeEventListener({ eventName: 'nonexistent', key: 999 });
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should only remove the specified listener', () => {
|
|
53
|
+
const bus = new LWEventBus();
|
|
54
|
+
const listener1 = bus.addEventListener('test', () => {});
|
|
55
|
+
bus.addEventListener('test', () => {});
|
|
56
|
+
bus.removeEventListener(listener1);
|
|
57
|
+
assert.equal(Object.keys(bus.listeners['test']).length, 1);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('dispatchEvent', () => {
|
|
62
|
+
it('should call listener callback with event data', async () => {
|
|
63
|
+
const bus = new LWEventBus();
|
|
64
|
+
let receivedEvent = null;
|
|
65
|
+
bus.addEventListener('test', event => {
|
|
66
|
+
receivedEvent = event;
|
|
67
|
+
});
|
|
68
|
+
bus.dispatchEvent('test', { message: 'hello' });
|
|
69
|
+
// dispatchEvent uses setTimeout, so we need to wait
|
|
70
|
+
await new Promise(r => setTimeout(r, 50));
|
|
71
|
+
assert.ok(receivedEvent);
|
|
72
|
+
assert.equal(receivedEvent.eventName, 'test');
|
|
73
|
+
assert.deepEqual(receivedEvent.data, { message: 'hello' });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should call all listeners for the event', async () => {
|
|
77
|
+
const bus = new LWEventBus();
|
|
78
|
+
let count = 0;
|
|
79
|
+
bus.addEventListener('test', () => count++);
|
|
80
|
+
bus.addEventListener('test', () => count++);
|
|
81
|
+
bus.dispatchEvent('test');
|
|
82
|
+
await new Promise(r => setTimeout(r, 50));
|
|
83
|
+
assert.equal(count, 2);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should not throw when dispatching event with no listeners', () => {
|
|
87
|
+
const bus = new LWEventBus();
|
|
88
|
+
assert.doesNotThrow(() => {
|
|
89
|
+
bus.dispatchEvent('nonexistent', { data: 'test' });
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should default data to null', async () => {
|
|
94
|
+
const bus = new LWEventBus();
|
|
95
|
+
let receivedEvent = null;
|
|
96
|
+
bus.addEventListener('test', event => {
|
|
97
|
+
receivedEvent = event;
|
|
98
|
+
});
|
|
99
|
+
bus.dispatchEvent('test');
|
|
100
|
+
await new Promise(r => setTimeout(r, 50));
|
|
101
|
+
assert.equal(receivedEvent.data, null);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import * as parser from '@babel/parser';
|
|
4
|
+
import { evaluate } from '../templates/lib/lw-expr-parser.js';
|
|
5
|
+
|
|
6
|
+
const getAST = expr => {
|
|
7
|
+
const parsedProgram = parser.parse(expr).program;
|
|
8
|
+
if (parsedProgram.directives.length > 0 && parsedProgram.body.length === 0) {
|
|
9
|
+
return parsedProgram.directives;
|
|
10
|
+
}
|
|
11
|
+
return parsedProgram.body;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const eval1 = (expr, context = {}) => evaluate(getAST(expr), context)[0];
|
|
15
|
+
|
|
16
|
+
describe('lw-expr-parser', () => {
|
|
17
|
+
describe('literals', () => {
|
|
18
|
+
it('should evaluate numeric literals', () => {
|
|
19
|
+
assert.equal(eval1('42'), 42);
|
|
20
|
+
assert.equal(eval1('3.14'), 3.14);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should evaluate string literals', () => {
|
|
24
|
+
assert.equal(eval1('"hello"'), 'hello');
|
|
25
|
+
assert.equal(eval1("'world'"), 'world');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should evaluate boolean literals', () => {
|
|
29
|
+
assert.equal(eval1('true'), true);
|
|
30
|
+
assert.equal(eval1('false'), false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should evaluate null literal', () => {
|
|
34
|
+
assert.equal(eval1('null'), null);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should evaluate regex literals', () => {
|
|
38
|
+
const result = eval1('/abc/gi');
|
|
39
|
+
assert.ok(result instanceof RegExp);
|
|
40
|
+
assert.equal(result.source, 'abc');
|
|
41
|
+
assert.equal(result.flags, 'gi');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('binary operations', () => {
|
|
46
|
+
it('should handle arithmetic', () => {
|
|
47
|
+
assert.equal(eval1('2 + 3'), 5);
|
|
48
|
+
assert.equal(eval1('10 - 4'), 6);
|
|
49
|
+
assert.equal(eval1('3 * 4'), 12);
|
|
50
|
+
assert.equal(eval1('15 / 3'), 5);
|
|
51
|
+
assert.equal(eval1('10 % 3'), 1);
|
|
52
|
+
assert.equal(eval1('2 ** 3'), 8);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should handle comparison operators', () => {
|
|
56
|
+
assert.equal(eval1('1 == 1'), true);
|
|
57
|
+
assert.equal(eval1('1 === 1'), true);
|
|
58
|
+
assert.equal(eval1('1 != 2'), true);
|
|
59
|
+
assert.equal(eval1('1 !== 2'), true);
|
|
60
|
+
assert.equal(eval1('1 < 2'), true);
|
|
61
|
+
assert.equal(eval1('2 <= 2'), true);
|
|
62
|
+
assert.equal(eval1('3 > 2'), true);
|
|
63
|
+
assert.equal(eval1('3 >= 3'), true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should handle bitwise operators', () => {
|
|
67
|
+
assert.equal(eval1('5 & 3'), 1);
|
|
68
|
+
assert.equal(eval1('5 | 3'), 7);
|
|
69
|
+
assert.equal(eval1('5 ^ 3'), 6);
|
|
70
|
+
assert.equal(eval1('1 << 2'), 4);
|
|
71
|
+
assert.equal(eval1('8 >> 2'), 2);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should handle string concatenation', () => {
|
|
75
|
+
assert.equal(eval1('"hello" + " " + "world"'), 'hello world');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('unary operations', () => {
|
|
80
|
+
it('should handle negation', () => {
|
|
81
|
+
assert.equal(eval1('-5'), -5);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should handle logical not', () => {
|
|
85
|
+
assert.equal(eval1('!true'), false);
|
|
86
|
+
assert.equal(eval1('!false'), true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should handle typeof', () => {
|
|
90
|
+
assert.equal(eval1('typeof 42'), 'number');
|
|
91
|
+
assert.equal(eval1('typeof "hi"'), 'string');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should handle void', () => {
|
|
95
|
+
assert.equal(eval1('void 0'), undefined);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should handle unary plus', () => {
|
|
99
|
+
assert.equal(eval1('+"5"'), 5);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should handle bitwise NOT', () => {
|
|
103
|
+
assert.equal(eval1('~0'), -1);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('logical operations', () => {
|
|
108
|
+
it('should handle && operator', () => {
|
|
109
|
+
assert.equal(eval1('true && "yes"'), 'yes');
|
|
110
|
+
assert.equal(eval1('false && "yes"'), false);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should handle || operator', () => {
|
|
114
|
+
assert.equal(eval1('false || "fallback"'), 'fallback');
|
|
115
|
+
assert.equal(eval1('"first" || "second"'), 'first');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should handle ?? operator', () => {
|
|
119
|
+
assert.equal(eval1('null ?? "default"'), 'default');
|
|
120
|
+
assert.equal(eval1('0 ?? "default"'), 0);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('conditional (ternary) expression', () => {
|
|
125
|
+
it('should return consequent when true', () => {
|
|
126
|
+
assert.equal(eval1('true ? "yes" : "no"'), 'yes');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should return alternate when false', () => {
|
|
130
|
+
assert.equal(eval1('false ? "yes" : "no"'), 'no');
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('identifiers and context', () => {
|
|
135
|
+
it('should resolve identifiers from context object', () => {
|
|
136
|
+
assert.equal(eval1('name', { name: 'Alice' }), 'Alice');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should resolve identifiers from context array', () => {
|
|
140
|
+
assert.equal(eval1('x', [{ x: 10 }, { y: 20 }]), 10);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should return undefined for missing identifiers', () => {
|
|
144
|
+
assert.equal(eval1('missing', {}), undefined);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should resolve from first matching context in array', () => {
|
|
148
|
+
assert.equal(eval1('x', [{ x: 'first' }, { x: 'second' }]), 'first');
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe('member expressions', () => {
|
|
153
|
+
it('should access object properties', () => {
|
|
154
|
+
assert.equal(eval1('obj.name', { obj: { name: 'test' } }), 'test');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should access computed properties', () => {
|
|
158
|
+
assert.equal(eval1('obj["key"]', { obj: { key: 'value' } }), 'value');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should access nested properties', () => {
|
|
162
|
+
assert.equal(eval1('a.b.c', { a: { b: { c: 42 } } }), 42);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should bind functions to their object', () => {
|
|
166
|
+
const result = eval1('arr.join', { arr: [1, 2, 3] });
|
|
167
|
+
assert.equal(typeof result, 'function');
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe('optional member expressions', () => {
|
|
172
|
+
it('should return undefined for null object', () => {
|
|
173
|
+
assert.equal(eval1('obj?.name', { obj: null }), undefined);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('should return undefined for undefined object', () => {
|
|
177
|
+
assert.equal(eval1('obj?.name', {}), undefined);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should return value when object exists', () => {
|
|
181
|
+
assert.equal(eval1('obj?.name', { obj: { name: 'test' } }), 'test');
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe('function calls', () => {
|
|
186
|
+
it('should call functions', () => {
|
|
187
|
+
const ctx = { greet: name => `Hello ${name}` };
|
|
188
|
+
assert.equal(eval1('greet("World")', ctx), 'Hello World');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should call method on object', () => {
|
|
192
|
+
assert.equal(eval1('arr.join(",")', { arr: [1, 2, 3] }), '1,2,3');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should handle spread arguments', () => {
|
|
196
|
+
const ctx = { sum: (...args) => args.reduce((a, b) => a + b, 0), nums: [1, 2, 3] };
|
|
197
|
+
assert.equal(eval1('sum(...nums)', ctx), 6);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should call string methods', () => {
|
|
201
|
+
assert.equal(eval1('name.toUpperCase()', { name: 'hello' }), 'HELLO');
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe('assignment operations', () => {
|
|
206
|
+
it('should handle simple assignment', () => {
|
|
207
|
+
const ctx = { x: 0 };
|
|
208
|
+
eval1('x = 5', ctx);
|
|
209
|
+
assert.equal(ctx.x, 5);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should handle += assignment', () => {
|
|
213
|
+
const ctx = { x: 10 };
|
|
214
|
+
eval1('x += 5', ctx);
|
|
215
|
+
assert.equal(ctx.x, 15);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('should handle -= assignment', () => {
|
|
219
|
+
const ctx = { x: 10 };
|
|
220
|
+
eval1('x -= 3', ctx);
|
|
221
|
+
assert.equal(ctx.x, 7);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('should handle member expression assignment', () => {
|
|
225
|
+
const ctx = { obj: { val: 0 } };
|
|
226
|
+
eval1('obj.val = 42', ctx);
|
|
227
|
+
assert.equal(ctx.obj.val, 42);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe('update operations', () => {
|
|
232
|
+
it('should handle prefix increment', () => {
|
|
233
|
+
const ctx = { x: 5 };
|
|
234
|
+
const result = eval1('++x', ctx);
|
|
235
|
+
assert.equal(result, 6);
|
|
236
|
+
assert.equal(ctx.x, 6);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should handle postfix increment', () => {
|
|
240
|
+
const ctx = { x: 5 };
|
|
241
|
+
const result = eval1('x++', ctx);
|
|
242
|
+
assert.equal(result, 5);
|
|
243
|
+
assert.equal(ctx.x, 6);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('should handle prefix decrement', () => {
|
|
247
|
+
const ctx = { x: 5 };
|
|
248
|
+
const result = eval1('--x', ctx);
|
|
249
|
+
assert.equal(result, 4);
|
|
250
|
+
assert.equal(ctx.x, 4);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('should handle postfix decrement', () => {
|
|
254
|
+
const ctx = { x: 5 };
|
|
255
|
+
const result = eval1('x--', ctx);
|
|
256
|
+
assert.equal(result, 5);
|
|
257
|
+
assert.equal(ctx.x, 4);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
describe('array expressions', () => {
|
|
262
|
+
it('should create arrays', () => {
|
|
263
|
+
assert.deepEqual(eval1('[1, 2, 3]'), [1, 2, 3]);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('should handle spread in arrays', () => {
|
|
267
|
+
assert.deepEqual(eval1('[...arr, 4]', { arr: [1, 2, 3] }), [1, 2, 3, 4]);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
describe('object expressions', () => {
|
|
272
|
+
it('should create objects', () => {
|
|
273
|
+
assert.deepEqual(eval1('({ a: 1, b: 2 })'), { a: 1, b: 2 });
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('should handle computed keys', () => {
|
|
277
|
+
assert.deepEqual(eval1('({ [key]: "value" })', { key: 'dynamic' }), { dynamic: 'value' });
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
describe('new expressions', () => {
|
|
282
|
+
it('should create new instances', () => {
|
|
283
|
+
const result = eval1('new Date(2024, 0, 1)', { Date });
|
|
284
|
+
assert.ok(result instanceof Date);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
describe('this expression', () => {
|
|
289
|
+
it('should resolve this from context', () => {
|
|
290
|
+
const self = { name: 'component' };
|
|
291
|
+
assert.equal(eval1('this.name', [{ this: self }]), 'component');
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
describe('error handling', () => {
|
|
296
|
+
it('should throw with location info on error', () => {
|
|
297
|
+
const loc = { startLine: 1, endLine: 1 };
|
|
298
|
+
assert.throws(() => {
|
|
299
|
+
evaluate(getAST('nonexistent()'), {}, loc);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { describe, it, beforeEach } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { parse } from '../lib/lw-html-parser.js';
|
|
4
|
+
|
|
5
|
+
describe('lw-html-parser', () => {
|
|
6
|
+
describe('parse', () => {
|
|
7
|
+
it('should return an object with html property', () => {
|
|
8
|
+
const result = parse('<div>hello</div>');
|
|
9
|
+
assert.ok(result.html);
|
|
10
|
+
assert.equal(typeof result.html, 'string');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should pass through plain HTML unchanged', () => {
|
|
14
|
+
const result = parse('<div>hello</div>');
|
|
15
|
+
assert.equal(result.html, '<div>hello</div>');
|
|
16
|
+
// Only the html key, no interpolation keys
|
|
17
|
+
assert.deepEqual(Object.keys(result), ['html']);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should handle lw text interpolation', () => {
|
|
21
|
+
const result = parse('<span lw>name</span>');
|
|
22
|
+
const keys = Object.keys(result).filter(k => k !== 'html');
|
|
23
|
+
assert.equal(keys.length, 1);
|
|
24
|
+
|
|
25
|
+
const interp = result[keys[0]];
|
|
26
|
+
assert.ok(interp.ast);
|
|
27
|
+
assert.ok(interp.loc);
|
|
28
|
+
// The content should be cleared from the HTML
|
|
29
|
+
assert.ok(!result.html.includes('>name<'));
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should handle lw with empty content', () => {
|
|
33
|
+
const result = parse('<span lw></span>');
|
|
34
|
+
const keys = Object.keys(result).filter(k => k !== 'html');
|
|
35
|
+
assert.equal(keys.length, 1);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should handle lw with complex expressions', () => {
|
|
39
|
+
const result = parse('<span lw>1 + 2 + 3</span>');
|
|
40
|
+
const keys = Object.keys(result).filter(k => k !== 'html');
|
|
41
|
+
assert.equal(keys.length, 1);
|
|
42
|
+
|
|
43
|
+
const interp = result[keys[0]];
|
|
44
|
+
assert.ok(interp.ast.length > 0);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should handle lw-on:click event binding', () => {
|
|
48
|
+
const result = parse('<button lw-on:click="handleClick()">Click</button>');
|
|
49
|
+
const keys = Object.keys(result).filter(k => k !== 'html');
|
|
50
|
+
assert.equal(keys.length, 1);
|
|
51
|
+
|
|
52
|
+
const interp = result[keys[0]];
|
|
53
|
+
assert.equal(interp.lwType, 'lw-on');
|
|
54
|
+
assert.equal(interp.lwValue, 'click');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should handle lw-class binding', () => {
|
|
58
|
+
const result = parse('<div lw-class:active="isActive"></div>');
|
|
59
|
+
const keys = Object.keys(result).filter(k => k !== 'html');
|
|
60
|
+
assert.equal(keys.length, 1);
|
|
61
|
+
|
|
62
|
+
const interp = result[keys[0]];
|
|
63
|
+
assert.equal(interp.lwType, 'lw-class');
|
|
64
|
+
assert.equal(interp.lwValue, 'active');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should handle lw-bind binding', () => {
|
|
68
|
+
const result = parse('<input lw-bind:value="name">');
|
|
69
|
+
const keys = Object.keys(result).filter(k => k !== 'html');
|
|
70
|
+
assert.equal(keys.length, 1);
|
|
71
|
+
|
|
72
|
+
const interp = result[keys[0]];
|
|
73
|
+
assert.equal(interp.lwType, 'lw-bind');
|
|
74
|
+
assert.equal(interp.lwValue, 'value');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should handle lw-bind:class with existing class attr', () => {
|
|
78
|
+
const result = parse('<div class="base" lw-bind:class="dynamicClass"></div>');
|
|
79
|
+
assert.ok(result.html.includes('lw-init-class'));
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should handle lw-model binding', () => {
|
|
83
|
+
const result = parse('<input lw-model="username">');
|
|
84
|
+
const keys = Object.keys(result).filter(k => k !== 'html');
|
|
85
|
+
assert.equal(keys.length, 1);
|
|
86
|
+
|
|
87
|
+
// lw-model adds both lw-elem-bind and lw-elem
|
|
88
|
+
assert.ok(result.html.includes('lw-elem-bind'));
|
|
89
|
+
assert.ok(result.html.includes('lw-elem'));
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should handle lw-for loop', () => {
|
|
93
|
+
const result = parse('<li lw-for="item in items">text</li>');
|
|
94
|
+
const keys = Object.keys(result).filter(k => k !== 'html');
|
|
95
|
+
assert.equal(keys.length, 1);
|
|
96
|
+
|
|
97
|
+
const interp = result[keys[0]];
|
|
98
|
+
assert.equal(interp.itemExpr, 'item');
|
|
99
|
+
assert.equal(interp.itemsExpr, 'items');
|
|
100
|
+
assert.ok(interp.astItems);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should handle lw-for with index', () => {
|
|
104
|
+
const result = parse('<li lw-for="item, index in items">text</li>');
|
|
105
|
+
const keys = Object.keys(result).filter(k => k !== 'html');
|
|
106
|
+
const interp = result[keys[0]];
|
|
107
|
+
assert.equal(interp.itemExpr, 'item');
|
|
108
|
+
assert.equal(interp.indexExpr, 'index');
|
|
109
|
+
assert.equal(interp.itemsExpr, 'items');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should handle lw-input binding', () => {
|
|
113
|
+
const result = parse('<input lw-input:change="onInputChange()">');
|
|
114
|
+
const keys = Object.keys(result).filter(k => k !== 'html');
|
|
115
|
+
assert.equal(keys.length, 1);
|
|
116
|
+
|
|
117
|
+
// lw-input adds lw-elem-bind
|
|
118
|
+
assert.ok(result.html.includes('lw-elem-bind'));
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should handle multiple lw attributes on different elements', () => {
|
|
122
|
+
const result = parse('<div><span lw>a</span><span lw>b</span></div>');
|
|
123
|
+
const keys = Object.keys(result).filter(k => k !== 'html');
|
|
124
|
+
assert.equal(keys.length, 2);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should handle nested elements with lw attributes', () => {
|
|
128
|
+
const result = parse('<div lw-on:click="handler()"><span lw>text</span></div>');
|
|
129
|
+
const keys = Object.keys(result).filter(k => k !== 'html');
|
|
130
|
+
assert.equal(keys.length, 2);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should strip AST location info', () => {
|
|
134
|
+
const result = parse('<span lw>name</span>');
|
|
135
|
+
const keys = Object.keys(result).filter(k => k !== 'html');
|
|
136
|
+
const interp = result[keys[0]];
|
|
137
|
+
const astStr = JSON.stringify(interp.ast);
|
|
138
|
+
// Should not contain loc, start, end from babel
|
|
139
|
+
assert.ok(!astStr.includes('"loc"'));
|
|
140
|
+
assert.ok(!astStr.includes('"start"'));
|
|
141
|
+
assert.ok(!astStr.includes('"end"'));
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should handle generic lw- prefixed attributes', () => {
|
|
145
|
+
const result = parse('<div lw-if="visible"></div>');
|
|
146
|
+
const keys = Object.keys(result).filter(k => k !== 'html');
|
|
147
|
+
assert.equal(keys.length, 1);
|
|
148
|
+
assert.ok(result.html.includes('lw-elem'));
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { getComponentName, getComponentPath, getPathLevels, throttle, portInUse } from '../commands/utils.js';
|
|
4
|
+
|
|
5
|
+
describe('utils', () => {
|
|
6
|
+
describe('getComponentName', () => {
|
|
7
|
+
it('should return the component name after last slash', () => {
|
|
8
|
+
assert.equal(getComponentName('path/to/my-component'), 'my-component');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should return the full string when no slash', () => {
|
|
12
|
+
assert.equal(getComponentName('my-component'), 'my-component');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should handle multiple slashes', () => {
|
|
16
|
+
assert.equal(getComponentName('a/b/c/deep-component'), 'deep-component');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should handle trailing component after single slash', () => {
|
|
20
|
+
assert.equal(getComponentName('dir/comp'), 'comp');
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('getComponentPath', () => {
|
|
25
|
+
it('should return the path before last slash', () => {
|
|
26
|
+
assert.equal(getComponentPath('path/to/my-component'), 'path/to');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should return empty string when no slash', () => {
|
|
30
|
+
assert.equal(getComponentPath('my-component'), '');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should handle single directory', () => {
|
|
34
|
+
assert.equal(getComponentPath('dir/comp'), 'dir');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('getPathLevels', () => {
|
|
39
|
+
it('should return empty string for file without directory', () => {
|
|
40
|
+
assert.equal(getPathLevels('file.html'), '');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should return ../ for one level deep', () => {
|
|
44
|
+
assert.equal(getPathLevels('dir/file.html'), '../');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should return ../../ for two levels deep', () => {
|
|
48
|
+
assert.equal(getPathLevels('a/b/file.html'), '../../');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should handle deeper paths', () => {
|
|
52
|
+
assert.equal(getPathLevels('a/b/c/file.html'), '../../../');
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('throttle', () => {
|
|
57
|
+
it('should call function after delay', async () => {
|
|
58
|
+
let called = false;
|
|
59
|
+
const fn = throttle(() => { called = true; }, 50);
|
|
60
|
+
fn();
|
|
61
|
+
assert.equal(called, false); // not called immediately
|
|
62
|
+
await new Promise(r => setTimeout(r, 100));
|
|
63
|
+
assert.equal(called, true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should not call function again within the limit', async () => {
|
|
67
|
+
let callCount = 0;
|
|
68
|
+
const fn = throttle(() => { callCount++; }, 50);
|
|
69
|
+
fn();
|
|
70
|
+
fn();
|
|
71
|
+
fn();
|
|
72
|
+
await new Promise(r => setTimeout(r, 100));
|
|
73
|
+
assert.equal(callCount, 1);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should allow call again after limit expires', async () => {
|
|
77
|
+
let callCount = 0;
|
|
78
|
+
const fn = throttle(() => { callCount++; }, 30);
|
|
79
|
+
fn();
|
|
80
|
+
await new Promise(r => setTimeout(r, 50));
|
|
81
|
+
assert.equal(callCount, 1);
|
|
82
|
+
fn();
|
|
83
|
+
await new Promise(r => setTimeout(r, 50));
|
|
84
|
+
assert.equal(callCount, 2);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('portInUse', () => {
|
|
89
|
+
it('should return false for an available port', async () => {
|
|
90
|
+
const result = await portInUse(0);
|
|
91
|
+
assert.equal(result, false);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
});
|