ripple 0.2.170 → 0.2.172

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.
@@ -0,0 +1,562 @@
1
+ import { track, flushSync, on } from 'ripple';
2
+
3
+ describe('on() event handler', () => {
4
+ it('should attach multiple handlers via onClick attribute (delegated)', () => {
5
+ component Basic() {
6
+ let count1 = track(0);
7
+ let count2 = track(0);
8
+
9
+ <button
10
+ onClick={() => {
11
+ @count1++;
12
+ @count2++;
13
+ }}
14
+ >
15
+ {'Click me'}
16
+ </button>
17
+ <div class="count1">{@count1}</div>
18
+ <div class="count2">{@count2}</div>
19
+ }
20
+
21
+ render(Basic);
22
+
23
+ const button = container.querySelector('button');
24
+ const count1Div = container.querySelector('.count1');
25
+ const count2Div = container.querySelector('.count2');
26
+
27
+ expect(count1Div.textContent).toBe('0');
28
+ expect(count2Div.textContent).toBe('0');
29
+
30
+ button.click();
31
+ flushSync();
32
+ expect(count1Div.textContent).toBe('1');
33
+ expect(count2Div.textContent).toBe('1');
34
+ });
35
+
36
+ it('should attach and remove a single event handler', () => {
37
+ component Basic() {
38
+ let count = track(0);
39
+
40
+ const setupListener = (node) => {
41
+ const remove = on(node, 'click', () => {
42
+ @count++;
43
+ });
44
+ return remove;
45
+ };
46
+ <button {ref setupListener}>{'Click me'}</button>
47
+ <div class="count">{@count}</div>
48
+ }
49
+
50
+ render(Basic);
51
+ flushSync();
52
+
53
+ const button = container.querySelector('button');
54
+ const countDiv = container.querySelector('.count');
55
+
56
+ expect(countDiv.textContent).toBe('0');
57
+
58
+ button.click();
59
+ flushSync();
60
+ expect(countDiv.textContent).toBe('1');
61
+
62
+ button.click();
63
+ flushSync();
64
+ expect(countDiv.textContent).toBe('2');
65
+ });
66
+
67
+ it('should handle multiple different event types on same element', () => {
68
+ component Basic() {
69
+ let clickCount = track(0);
70
+ let mousedownCount = track(0);
71
+
72
+ const setupListeners = (node) => {
73
+ const remove1 = on(node, 'click', () => {
74
+ @clickCount++;
75
+ });
76
+ const remove2 = on(node, 'mousedown', () => {
77
+ @mousedownCount++;
78
+ });
79
+ return () => {
80
+ remove1();
81
+ remove2();
82
+ };
83
+ };
84
+ <button {ref setupListeners}>{'Test'}</button>
85
+ <div class="click-count">{@clickCount}</div>
86
+ <div class="mousedown-count">{@mousedownCount}</div>
87
+ }
88
+
89
+ render(Basic);
90
+ flushSync();
91
+
92
+ const button = container.querySelector('button');
93
+ const clickDiv = container.querySelector('.click-count');
94
+ const mousedownDiv = container.querySelector('.mousedown-count');
95
+
96
+ expect(clickDiv.textContent).toBe('0');
97
+ expect(mousedownDiv.textContent).toBe('0');
98
+
99
+ button.click();
100
+ flushSync();
101
+ expect(clickDiv.textContent).toBe('1');
102
+ expect(mousedownDiv.textContent).toBe('0'); // click() doesn't trigger mousedown
103
+
104
+ button.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
105
+ flushSync();
106
+ expect(clickDiv.textContent).toBe('1');
107
+ expect(mousedownDiv.textContent).toBe('1');
108
+
109
+ // Click again to verify both handlers still work
110
+ button.click();
111
+ flushSync();
112
+ expect(clickDiv.textContent).toBe('2');
113
+ expect(mousedownDiv.textContent).toBe('1'); // Still only incremented by mousedown events
114
+ });
115
+
116
+ it('should handle multiple handlers for same event type on same element', () => {
117
+ component Basic() {
118
+ let callOrder = track<number[]>([]);
119
+
120
+ const setupListeners = (node) => {
121
+ const remove1 = on(node, 'click', () => {
122
+ @callOrder = [...@callOrder, 1];
123
+ });
124
+ const remove2 = on(node, 'click', () => {
125
+ @callOrder = [...@callOrder, 2];
126
+ });
127
+ const remove3 = on(node, 'click', () => {
128
+ @callOrder = [...@callOrder, 3];
129
+ });
130
+ return () => {
131
+ remove1();
132
+ remove2();
133
+ remove3();
134
+ };
135
+ };
136
+ <button {ref setupListeners}>{'Click me'}</button>
137
+ <div class="order">{@callOrder.join(',')}</div>
138
+ }
139
+
140
+ render(Basic);
141
+ flushSync();
142
+
143
+ const button = container.querySelector('button');
144
+ const orderDiv = container.querySelector('.order');
145
+
146
+ expect(orderDiv.textContent).toBe('');
147
+
148
+ button.click();
149
+ flushSync();
150
+ expect(orderDiv.textContent).toBe('1,2,3');
151
+
152
+ // Click again to verify order is consistent
153
+ button.click();
154
+ flushSync();
155
+ expect(orderDiv.textContent).toBe('1,2,3,1,2,3');
156
+ });
157
+
158
+ it('should remove specific handler without affecting others', () => {
159
+ component Basic() {
160
+ let handler1Called = track(0);
161
+ let handler2Called = track(0);
162
+ let handler3Called = track(0);
163
+ let removeHandler2;
164
+
165
+ const setupListeners = (node) => {
166
+ const remove1 = on(node, 'click', () => {
167
+ @handler1Called++;
168
+ });
169
+ removeHandler2 = on(node, 'click', () => {
170
+ @handler2Called++;
171
+ });
172
+ const remove3 = on(node, 'click', () => {
173
+ @handler3Called++;
174
+ });
175
+ return () => {
176
+ remove1();
177
+ removeHandler2?.();
178
+ remove3();
179
+ };
180
+ };
181
+ <div>
182
+ <button class="test-btn" {ref setupListeners}>{'Click me'}</button>
183
+ <button
184
+ class="remove-btn"
185
+ onClick={() => {
186
+ removeHandler2?.();
187
+ removeHandler2 = undefined;
188
+ }}
189
+ >
190
+ {'Remove handler 2'}
191
+ </button>
192
+ <div class="h1">{@handler1Called}</div>
193
+ <div class="h2">{@handler2Called}</div>
194
+ <div class="h3">{@handler3Called}</div>
195
+ </div>
196
+ }
197
+
198
+ render(Basic);
199
+ flushSync();
200
+
201
+ const testBtn = container.querySelector('.test-btn');
202
+ const removeBtn = container.querySelector('.remove-btn');
203
+ const h1Div = container.querySelector('.h1');
204
+ const h2Div = container.querySelector('.h2');
205
+ const h3Div = container.querySelector('.h3');
206
+
207
+ // All handlers should be called initially
208
+ testBtn.click();
209
+ flushSync();
210
+ expect(h1Div.textContent).toBe('1');
211
+ expect(h2Div.textContent).toBe('1');
212
+ expect(h3Div.textContent).toBe('1');
213
+
214
+ // Remove handler 2
215
+ removeBtn.click();
216
+ flushSync();
217
+
218
+ // Only handlers 1 and 3 should be called
219
+ testBtn.click();
220
+ flushSync();
221
+ expect(h1Div.textContent).toBe('2');
222
+ expect(h2Div.textContent).toBe('1'); // Should not increment
223
+ expect(h3Div.textContent).toBe('2');
224
+
225
+ // Verify again
226
+ testBtn.click();
227
+ flushSync();
228
+ expect(h1Div.textContent).toBe('3');
229
+ expect(h2Div.textContent).toBe('1'); // Still should not increment
230
+ expect(h3Div.textContent).toBe('3');
231
+ });
232
+
233
+ it(
234
+ 'should handle change event with multiple handlers (like bindChecked and bindIndeterminate)',
235
+ () => {
236
+ component Basic() {
237
+ let checked = track(false);
238
+ let indeterminate = track(true);
239
+
240
+ const setupListeners = (node) => {
241
+ node.indeterminate = @indeterminate;
242
+ node.checked = @checked;
243
+
244
+ const remove1 = on(node, 'change', () => {
245
+ @checked = node.checked;
246
+ });
247
+ const remove2 = on(node, 'change', () => {
248
+ @indeterminate = node.indeterminate;
249
+ });
250
+ return () => {
251
+ remove1();
252
+ remove2();
253
+ };
254
+ };
255
+ <div>
256
+ <input type="checkbox" {ref setupListeners} />
257
+ <div class="checked">{@checked ? 'true' : 'false'}</div>
258
+ <div class="indeterminate">{@indeterminate ? 'true' : 'false'}</div>
259
+ </div>
260
+ }
261
+
262
+ render(Basic);
263
+ flushSync();
264
+
265
+ const input = container.querySelector('input');
266
+ const checkedDiv = container.querySelector('.checked');
267
+ const indeterminateDiv = container.querySelector('.indeterminate');
268
+
269
+ expect(checkedDiv.textContent).toBe('false');
270
+ expect(indeterminateDiv.textContent).toBe('true');
271
+ expect(input.indeterminate).toBe(true);
272
+
273
+ // Click the checkbox
274
+ input.click();
275
+ flushSync();
276
+
277
+ // Both tracked values should update
278
+ expect(checkedDiv.textContent).toBe('true');
279
+ expect(indeterminateDiv.textContent).toBe('false');
280
+ },
281
+ );
282
+
283
+ it('should support non-delegated events', () => {
284
+ component Basic() {
285
+ let focusCount = track(0);
286
+
287
+ const setupListener = (node) => {
288
+ const remove = on(node, 'focus', () => {
289
+ @focusCount++;
290
+ });
291
+ return remove;
292
+ };
293
+
294
+ <input {ref setupListener} />
295
+ <div class="focus-count">{@focusCount}</div>
296
+ }
297
+
298
+ render(Basic);
299
+ flushSync();
300
+
301
+ const input = container.querySelector('input');
302
+ const focusDiv = container.querySelector('.focus-count');
303
+
304
+ expect(focusDiv.textContent).toBe('0');
305
+
306
+ input.dispatchEvent(new Event('focus'));
307
+ flushSync();
308
+ expect(focusDiv.textContent).toBe('1');
309
+
310
+ input.dispatchEvent(new Event('focus'));
311
+ flushSync();
312
+ expect(focusDiv.textContent).toBe('2');
313
+ });
314
+
315
+ it('should handle removal of all handlers for same event type', () => {
316
+ component Basic() {
317
+ let count = track(0);
318
+ let remove1, remove2, remove3;
319
+
320
+ const setupListeners = (node) => {
321
+ remove1 = on(node, 'click', () => {
322
+ @count++;
323
+ });
324
+ remove2 = on(node, 'click', () => {
325
+ @count += 10;
326
+ });
327
+ remove3 = on(node, 'click', () => {
328
+ @count += 100;
329
+ });
330
+ return () => {
331
+ remove1?.();
332
+ remove2?.();
333
+ remove3?.();
334
+ };
335
+ };
336
+ <div>
337
+ <button class="test-btn" {ref setupListeners}>{'Click me'}</button>
338
+ <button
339
+ class="remove-all"
340
+ onClick={() => {
341
+ remove1?.();
342
+ remove2?.();
343
+ remove3?.();
344
+ remove1 = remove2 = remove3 = undefined;
345
+ }}
346
+ >
347
+ {'Remove all'}
348
+ </button>
349
+ <div class="count">{@count}</div>
350
+ </div>
351
+ }
352
+
353
+ render(Basic);
354
+ flushSync();
355
+
356
+ const testBtn = container.querySelector('.test-btn');
357
+ const removeAllBtn = container.querySelector('.remove-all');
358
+ const countDiv = container.querySelector('.count');
359
+
360
+ expect(countDiv.textContent).toBe('0');
361
+
362
+ // All three handlers should fire (1 + 10 + 100 = 111)
363
+ testBtn.click();
364
+ flushSync();
365
+ expect(countDiv.textContent).toBe('111');
366
+
367
+ // Remove all handlers
368
+ removeAllBtn.click();
369
+ flushSync();
370
+
371
+ // No handlers should fire
372
+ testBtn.click();
373
+ flushSync();
374
+ expect(countDiv.textContent).toBe('111'); // Should remain unchanged
375
+ });
376
+
377
+ it('should not add duplicate handlers when same handler is attached multiple times', () => {
378
+ component Basic() {
379
+ let count = track(0);
380
+
381
+ const sharedHandler = () => {
382
+ @count++;
383
+ };
384
+
385
+ const setupListeners = (node) => {
386
+ // Attach the same handler multiple times
387
+ const remove1 = on(node, 'click', sharedHandler);
388
+ const remove2 = on(node, 'click', sharedHandler);
389
+ const remove3 = on(node, 'click', sharedHandler);
390
+
391
+ return () => {
392
+ remove1?.();
393
+ remove2?.();
394
+ remove3?.();
395
+ };
396
+ };
397
+
398
+ <button {ref setupListeners}>{'Click me'}</button>
399
+ <div class="count">{@count}</div>
400
+ }
401
+
402
+ render(Basic);
403
+ flushSync();
404
+
405
+ const button = container.querySelector('button');
406
+ const countDiv = container.querySelector('.count');
407
+
408
+ expect(countDiv.textContent).toBe('0');
409
+
410
+ // Handler should only be called once per click, not three times
411
+ button.click();
412
+ flushSync();
413
+ expect(countDiv.textContent).toBe('1');
414
+
415
+ button.click();
416
+ flushSync();
417
+ expect(countDiv.textContent).toBe('2');
418
+ });
419
+
420
+ it('should allow duplicate handlers when delegated is false (no deduplication)', () => {
421
+ component Basic() {
422
+ let count = track(0);
423
+
424
+ const sharedHandler = () => {
425
+ @count++;
426
+ };
427
+
428
+ const setupListeners = (node) => {
429
+ // Attach the same handler multiple times with delegated: false
430
+ const remove1 = on(node, 'click', sharedHandler, { delegated: false });
431
+ const remove2 = on(node, 'click', sharedHandler, { delegated: false });
432
+ const remove3 = on(node, 'click', sharedHandler, { delegated: false });
433
+
434
+ return () => {
435
+ remove1?.();
436
+ remove2?.();
437
+ remove3?.();
438
+ };
439
+ };
440
+
441
+ <button {ref setupListeners}>{'Click me'}</button>
442
+ <div class="count">{@count}</div>
443
+ }
444
+
445
+ render(Basic);
446
+ flushSync();
447
+
448
+ const button = container.querySelector('button');
449
+ const countDiv = container.querySelector('.count');
450
+
451
+ expect(countDiv.textContent).toBe('0');
452
+
453
+ // Non-delegated events use addEventListener directly, which DOES allow duplicates
454
+ // So handler should be called 3 times per click
455
+ button.click();
456
+ flushSync();
457
+ expect(countDiv.textContent).toBe('3');
458
+
459
+ button.click();
460
+ flushSync();
461
+ expect(countDiv.textContent).toBe('6');
462
+ });
463
+
464
+ it('should fire capture event on parent before bubbling event on child', () => {
465
+ component Basic() {
466
+ let callOrder = track<string[]>([]);
467
+
468
+ const parentCaptureHandler = () => {
469
+ @callOrder = [...@callOrder, 'parent-capture'];
470
+ };
471
+
472
+ const childBubbleHandler = () => {
473
+ @callOrder = [...@callOrder, 'child-bubble'];
474
+ };
475
+
476
+ const setupParent = (node) => {
477
+ return on(node, 'clickCapture', parentCaptureHandler);
478
+ };
479
+
480
+ const setupChild = (node) => {
481
+ return on(node, 'click', childBubbleHandler);
482
+ };
483
+
484
+ <div {ref setupParent} class="parent">
485
+ <button {ref setupChild} class="child">{'Click me'}</button>
486
+ </div>
487
+ <div class="order">{@callOrder.join(',')}</div>
488
+ }
489
+
490
+ render(Basic);
491
+ flushSync();
492
+
493
+ const button = container.querySelector('.child');
494
+ const orderDiv = container.querySelector('.order');
495
+
496
+ expect(orderDiv.textContent).toBe('');
497
+
498
+ // Capture phase happens first (parent), then bubbling phase (child)
499
+ button.click();
500
+ flushSync();
501
+ expect(orderDiv.textContent).toBe('parent-capture,child-bubble');
502
+
503
+ // Click again to verify order is consistent
504
+ button.click();
505
+ flushSync();
506
+ expect(orderDiv.textContent).toBe('parent-capture,child-bubble,parent-capture,child-bubble');
507
+ });
508
+
509
+ it('should fire handler only once when once option is true', () => {
510
+ component Basic() {
511
+ let count = track(0);
512
+ let permanentCount = track(0);
513
+
514
+ const setupListeners = (node) => {
515
+ const onceHandler = on(node, 'click', () => {
516
+ @count++;
517
+ }, { once: true });
518
+
519
+ const permanentHandler = on(node, 'click', () => {
520
+ @permanentCount++;
521
+ });
522
+
523
+ return () => {
524
+ onceHandler?.();
525
+ permanentHandler?.();
526
+ };
527
+ };
528
+
529
+ <button {ref setupListeners}>{'Click me'}</button>
530
+ <div class="once-count">{@count}</div>
531
+ <div class="permanent-count">{@permanentCount}</div>
532
+ }
533
+
534
+ render(Basic);
535
+ flushSync();
536
+
537
+ const button = container.querySelector('button');
538
+ const onceDiv = container.querySelector('.once-count');
539
+ const permanentDiv = container.querySelector('.permanent-count');
540
+
541
+ expect(onceDiv.textContent).toBe('0');
542
+ expect(permanentDiv.textContent).toBe('0');
543
+
544
+ // First click: both handlers should fire
545
+ button.click();
546
+ flushSync();
547
+ expect(onceDiv.textContent).toBe('1');
548
+ expect(permanentDiv.textContent).toBe('1');
549
+
550
+ // Second click: only permanent handler should fire
551
+ button.click();
552
+ flushSync();
553
+ expect(onceDiv.textContent).toBe('1'); // Still 1
554
+ expect(permanentDiv.textContent).toBe('2');
555
+
556
+ // Third click: only permanent handler should fire
557
+ button.click();
558
+ flushSync();
559
+ expect(onceDiv.textContent).toBe('1'); // Still 1
560
+ expect(permanentDiv.textContent).toBe('3');
561
+ });
562
+ });