ripple 0.2.168 → 0.2.169

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.
@@ -1,37 +1,67 @@
1
- /** List of Element events that will be delegated */
2
- const DELEGATED_EVENTS = [
3
- 'beforeinput',
4
- 'click',
5
- 'change',
6
- 'dblclick',
7
- 'contextmenu',
8
- 'focusin',
9
- 'focusout',
10
- 'input',
11
- 'keydown',
12
- 'keyup',
13
- 'mousedown',
14
- 'mousemove',
15
- 'mouseout',
16
- 'mouseover',
17
- 'mouseup',
18
- 'pointerdown',
19
- 'pointermove',
20
- 'pointerout',
21
- 'pointerover',
22
- 'pointerup',
23
- 'touchend',
24
- 'touchmove',
25
- 'touchstart',
26
- ];
1
+ /** @import { AddEventObject } from '#public'*/
2
+
3
+ const NON_DELEGATED_EVENTS = new Set([
4
+ 'abort',
5
+ 'afterprint',
6
+ 'beforeprint',
7
+ 'beforetoggle',
8
+ 'beforeunload',
9
+ 'blur',
10
+ 'close',
11
+ 'command',
12
+ 'contextmenu',
13
+ 'cuechange',
14
+ 'DOMContentLoaded',
15
+ 'error',
16
+ 'focus',
17
+ 'invalid',
18
+ 'load',
19
+ 'loadend',
20
+ 'loadstart',
21
+ 'mouseenter',
22
+ 'mouseleave',
23
+ 'pointerenter',
24
+ 'pointerleave',
25
+ 'progress',
26
+ 'readystatechange',
27
+ 'resize',
28
+ 'scroll',
29
+ 'scrollend',
30
+ 'toggle',
31
+ 'unload',
32
+ 'visibilitychange',
33
+ // Media Events
34
+ 'canplay',
35
+ 'canplaythrough',
36
+ 'durationchange',
37
+ 'emptied',
38
+ 'encrypted',
39
+ 'ended',
40
+ 'loadeddata',
41
+ 'loadedmetadata',
42
+ 'loadstart',
43
+ 'pause',
44
+ 'play',
45
+ 'playing',
46
+ 'progress',
47
+ 'ratechange',
48
+ 'seeked',
49
+ 'seeking',
50
+ 'stalled',
51
+ 'suspend',
52
+ 'timeupdate',
53
+ 'volumechange',
54
+ 'waiting',
55
+ 'waitingforkey',
56
+ ]);
27
57
 
28
58
  /**
29
59
  * Checks if an event should be delegated
30
60
  * @param {string} event_name - The event name (e.g., 'click', 'focus')
31
61
  * @returns {boolean}
32
62
  */
33
- export function is_delegated(event_name) {
34
- return DELEGATED_EVENTS.includes(event_name);
63
+ export function is_non_delegated(event_name) {
64
+ return NON_DELEGATED_EVENTS.has(event_name);
35
65
  }
36
66
 
37
67
  /**
@@ -40,32 +70,79 @@ export function is_delegated(event_name) {
40
70
  * @returns {boolean}
41
71
  */
42
72
  export function is_event_attribute(attr) {
43
- return attr.startsWith('on') && attr.length > 2 && attr[2] === attr[2].toUpperCase();
73
+ return attr.startsWith('on') && attr.length > 2 && attr[2] === attr[2].toUpperCase();
74
+ }
75
+
76
+ /**
77
+ * Checks if the event is a capture event.
78
+ * @param {string} event_name - The event name.
79
+ * @returns {boolean}
80
+ */
81
+ export function is_capture_event(event_name) {
82
+ var lowered = event_name.toLowerCase();
83
+ return (
84
+ event_name.endsWith('Capture') &&
85
+ lowered !== 'gotpointercapture' &&
86
+ lowered !== 'lostpointercapture'
87
+ );
44
88
  }
45
89
 
46
90
  /**
91
+ * Retrieves the original event name from an event attribute.
47
92
  * @param {string} name
93
+ * @returns {string}
48
94
  */
49
- export function is_capture_event(name) {
50
- return (
51
- name.endsWith('Capture') &&
52
- name.toLowerCase() !== 'gotpointercapture' &&
53
- name.toLowerCase() !== 'lostpointercapture'
54
- );
95
+ export function get_original_event_name(name) {
96
+ return name.slice(2);
55
97
  }
56
98
 
57
99
  /**
100
+ * Normalizes the event name to lowercase.
101
+ * @param {string} name
102
+ * @returns {string}
103
+ */
104
+ export function normalize_event_name(name) {
105
+ return extract_event_name(name).toLowerCase();
106
+ }
107
+
108
+ /**
109
+ * Extracts the base event name from an event attribute.
110
+ * @param {string} name
111
+ * @returns {string}
112
+ */
113
+ function extract_event_name(name) {
114
+ name = get_original_event_name(name);
115
+
116
+ if (is_capture_event(name)) {
117
+ return event_name_from_capture(name);
118
+ }
119
+ return name;
120
+ }
121
+
122
+ /**
123
+ * Converts a capture event name to its base event name.
58
124
  * @param {string} event_name
125
+ * @returns {string}
59
126
  */
60
- export function get_attribute_event_name(event_name) {
61
- event_name = event_name.slice(2); // strip "on"
62
- if (is_capture_event(event_name)) {
63
- event_name = event_name.slice(0, -7); // strip "Capture"
64
- }
65
- return event_name.toLowerCase();
127
+ export function event_name_from_capture(event_name) {
128
+ return event_name.slice(0, -7); // strip "Capture"
129
+ }
130
+
131
+ /**
132
+ * Converts an event attribute name to the actual event name.
133
+ * @param {string} name
134
+ * @param {EventListener | AddEventObject} handler
135
+ * @returns {string}
136
+ */
137
+ export function get_attribute_event_name(name, handler) {
138
+ name = extract_event_name(name);
139
+
140
+ return typeof handler === 'object' && handler.customName
141
+ ? handler.customName
142
+ : name.toLowerCase();
66
143
  }
67
144
 
68
- const PASSIVE_EVENTS = ['touchstart', 'touchmove'];
145
+ const PASSIVE_EVENTS = ['touchstart', 'touchmove', 'wheel', 'mousewheel'];
69
146
 
70
147
  /**
71
148
  * Checks if an event is passive (e.g., 'touchstart', 'touchmove').
@@ -73,5 +150,5 @@ const PASSIVE_EVENTS = ['touchstart', 'touchmove'];
73
150
  * @returns {boolean}
74
151
  */
75
152
  export function is_passive_event(name) {
76
- return PASSIVE_EVENTS.includes(name);
153
+ return PASSIVE_EVENTS.includes(name);
77
154
  }
@@ -524,4 +524,446 @@ describe('basic client > attribute rendering', () => {
524
524
  render(App);
525
525
  expect(container).toMatchSnapshot();
526
526
  });
527
+
528
+ it('handles reactive event handler changes', () => {
529
+ component Basic() {
530
+ let count = track(0);
531
+ let mode = track<'increment' | 'decrement'>('increment');
532
+
533
+ const incrementHandler = () => {
534
+ @count++;
535
+ };
536
+
537
+ const decrementHandler = () => {
538
+ @count--;
539
+ };
540
+
541
+ <button
542
+ onClick={() => {
543
+ @mode = @mode === 'increment' ? 'decrement' : 'increment';
544
+ }}
545
+ class="toggle-mode"
546
+ >
547
+ {'Toggle Mode'}
548
+ </button>
549
+ <button onClick={@mode === 'increment' ? incrementHandler : decrementHandler} class="action">
550
+ {@mode === 'increment' ? '+' : '-'}
551
+ </button>
552
+ <div class="count">{@count}</div>
553
+ }
554
+
555
+ render(Basic);
556
+
557
+ const toggleBtn = container.querySelector('.toggle-mode');
558
+ const actionBtn = container.querySelector('.action');
559
+ const countDiv = container.querySelector('.count');
560
+
561
+ expect(actionBtn.textContent).toBe('+');
562
+ expect(countDiv.textContent).toBe('0');
563
+
564
+ actionBtn.click();
565
+ flushSync();
566
+ expect(countDiv.textContent).toBe('1');
567
+
568
+ actionBtn.click();
569
+ flushSync();
570
+ expect(countDiv.textContent).toBe('2');
571
+
572
+ toggleBtn.click();
573
+ flushSync();
574
+ expect(actionBtn.textContent).toBe('-');
575
+
576
+ actionBtn.click();
577
+ flushSync();
578
+ expect(countDiv.textContent).toBe('1');
579
+
580
+ actionBtn.click();
581
+ flushSync();
582
+ expect(countDiv.textContent).toBe('0');
583
+ });
584
+
585
+ it('handles events with capture option', () => {
586
+ component Basic() {
587
+ let captureOrder = #[];
588
+
589
+ const handleCaptureClick = {
590
+ handleEvent() {
591
+ captureOrder.push('capture');
592
+ },
593
+ capture: true,
594
+ };
595
+
596
+ const handleBubbleClick = () => {
597
+ captureOrder.push('bubble');
598
+ };
599
+
600
+ <div onClick={handleCaptureClick} class="outer">
601
+ <button onClick={handleBubbleClick} class="inner">{'Click'}</button>
602
+ <div class="order">{captureOrder.join(' -> ')}</div>
603
+ </div>
604
+ }
605
+
606
+ render(Basic);
607
+
608
+ const button = container.querySelector('.inner');
609
+ const orderDiv = container.querySelector('.order');
610
+
611
+ expect(orderDiv.textContent).toBe('');
612
+
613
+ // Test that capture fires before bubble
614
+ button.click();
615
+ flushSync();
616
+ expect(orderDiv.textContent).toBe('capture -> bubble');
617
+ });
618
+
619
+ it('handles events with Capture suffix in the name', () => {
620
+ component Basic() {
621
+ let captureOrder = #[];
622
+
623
+ const handleCaptureClick = () => {
624
+ captureOrder.push('capture');
625
+ };
626
+
627
+ const handleBubbleClick = () => {
628
+ captureOrder.push('bubble');
629
+ };
630
+
631
+ <div onClickCapture={handleCaptureClick} class="outer">
632
+ <button onClick={handleBubbleClick} class="inner">{'Click'}</button>
633
+ <div class="order">{captureOrder.join(' -> ')}</div>
634
+ </div>
635
+ }
636
+
637
+ render(Basic);
638
+
639
+ const button = container.querySelector('.inner');
640
+ const orderDiv = container.querySelector('.order');
641
+
642
+ expect(orderDiv.textContent).toBe('');
643
+
644
+ // Test that capture fires before bubble
645
+ button.click();
646
+ flushSync();
647
+ expect(orderDiv.textContent).toBe('capture -> bubble');
648
+ });
649
+
650
+ it('handles custom events with customName option', () => {
651
+ component Basic() {
652
+ let customEventCount = track(0);
653
+
654
+ const handleCustom = {
655
+ handleEvent(event: CustomEvent) {
656
+ @customEventCount += event.detail.value;
657
+ },
658
+ customName: 'MyCustomEvent',
659
+ };
660
+
661
+ <div>
662
+ <div onMyCustomEvent={handleCustom} class="custom-target">{'Custom'}</div>
663
+ <div class="custom-count">{@customEventCount}</div>
664
+ </div>
665
+ }
666
+
667
+ render(Basic);
668
+
669
+ const customTarget = container.querySelector('.custom-target');
670
+ const customCountDiv = container.querySelector('.custom-count');
671
+
672
+ expect(customCountDiv.textContent).toBe('0');
673
+
674
+ const customEvent = new CustomEvent('MyCustomEvent', { bubbles: true, detail: { value: 5 } });
675
+ customTarget.dispatchEvent(customEvent);
676
+ flushSync();
677
+ expect(customCountDiv.textContent).toBe('5');
678
+
679
+ const customEvent2 = new CustomEvent('MyCustomEvent', { bubbles: true, detail: { value: 3 } });
680
+ customTarget.dispatchEvent(customEvent2);
681
+ flushSync();
682
+ expect(customCountDiv.textContent).toBe('8');
683
+ });
684
+
685
+ it('handles events with delegated: false option to bypass delegation', () => {
686
+ component Basic() {
687
+ let delegatedCount = track(0);
688
+ let nonDelegatedCount = track(0);
689
+
690
+ const delegatedHandler = () => {
691
+ @delegatedCount++;
692
+ };
693
+
694
+ const nonDelegatedHandler = {
695
+ handleEvent() {
696
+ @nonDelegatedCount++;
697
+ },
698
+ delegated: false,
699
+ };
700
+
701
+ <div>
702
+ <button onClick={delegatedHandler} class="delegated-btn">{'Delegated'}</button>
703
+ <button onClick={nonDelegatedHandler} class="non-delegated-btn">{'Non-Delegated'}</button>
704
+ <div class="delegated-count">{@delegatedCount}</div>
705
+ <div class="non-delegated-count">{@nonDelegatedCount}</div>
706
+ </div>
707
+ }
708
+
709
+ render(Basic);
710
+
711
+ const delegatedBtn = container.querySelector('.delegated-btn');
712
+ const nonDelegatedBtn = container.querySelector('.non-delegated-btn');
713
+
714
+ // Check that delegated event has __click property set on the element
715
+ expect(delegatedBtn.__click).toBeDefined();
716
+ expect(typeof delegatedBtn.__click).toBe('function');
717
+
718
+ // Check that non-delegated event does NOT have __click property (it's attached directly)
719
+ expect(nonDelegatedBtn.__click).toBeUndefined();
720
+
721
+ // Verify both handlers work correctly
722
+ delegatedBtn.click();
723
+ flushSync();
724
+ expect(container.querySelector('.delegated-count').textContent).toBe('1');
725
+
726
+ nonDelegatedBtn.click();
727
+ flushSync();
728
+ expect(container.querySelector('.non-delegated-count').textContent).toBe('1');
729
+ });
730
+
731
+ it('measures effect of delegated vs non-delegated events', () => {
732
+ const delegatedClicks: number[] = [];
733
+ const nonDelegatedClicks: number[] = [];
734
+
735
+ component Basic() {
736
+ const makeDelegatedHandler = (id: number) => () => {
737
+ delegatedClicks.push(id);
738
+ };
739
+
740
+ const makeNonDelegatedHandler = (id: number) => ({
741
+ handleEvent() {
742
+ nonDelegatedClicks.push(id);
743
+ },
744
+ delegated: false,
745
+ });
746
+
747
+ const buttonIds = [0, 1, 2, 3, 4];
748
+
749
+ <div>
750
+ <div class="delegated-buttons">
751
+ for (const i of buttonIds) {
752
+ <button onClick={makeDelegatedHandler(i)} class={`delegated-${i}`}>{`D${i}`}</button>
753
+ }
754
+ </div>
755
+ <div class="non-delegated-buttons">
756
+ for (const i of buttonIds) {
757
+ <button onClick={makeNonDelegatedHandler(i)} class={`non-delegated-${i}`}>
758
+ {`ND${i}`}
759
+ </button>
760
+ }
761
+ </div>
762
+ </div>
763
+ }
764
+
765
+ render(Basic);
766
+
767
+ // Test delegated buttons
768
+ for (let i = 0; i < 5; i++) {
769
+ const btn = container.querySelector(`.delegated-${i}`);
770
+ btn.click();
771
+ flushSync();
772
+ }
773
+
774
+ expect(delegatedClicks).toEqual([0, 1, 2, 3, 4]);
775
+
776
+ // Test non-delegated buttons
777
+ for (let i = 0; i < 5; i++) {
778
+ const btn = container.querySelector(`.non-delegated-${i}`);
779
+ btn.click();
780
+ flushSync();
781
+ }
782
+
783
+ expect(nonDelegatedClicks).toEqual([0, 1, 2, 3, 4]);
784
+
785
+ // Verify both can be called in mixed order
786
+ delegatedClicks.length = 0;
787
+ nonDelegatedClicks.length = 0;
788
+
789
+ container.querySelector('.delegated-2').click();
790
+ container.querySelector('.non-delegated-1').click();
791
+ container.querySelector('.delegated-0').click();
792
+ container.querySelector('.non-delegated-3').click();
793
+ flushSync();
794
+
795
+ expect(delegatedClicks).toEqual([2, 0]);
796
+ expect(nonDelegatedClicks).toEqual([1, 3]);
797
+ });
798
+
799
+ it('handles events defined as function directly vs as object', () => {
800
+ component Basic() {
801
+ let functionCount = track(0);
802
+ let objectCount = track(0);
803
+
804
+ const functionHandler = () => {
805
+ @functionCount++;
806
+ };
807
+
808
+ const objectHandler = {
809
+ handleEvent() {
810
+ @objectCount++;
811
+ },
812
+ };
813
+
814
+ <div>
815
+ <button onClick={functionHandler} class="function-btn">{'Function'}</button>
816
+ <button onClick={objectHandler} class="object-btn">{'Object'}</button>
817
+ <div class="function-count">{@functionCount}</div>
818
+ <div class="object-count">{@objectCount}</div>
819
+ </div>
820
+ }
821
+
822
+ render(Basic);
823
+
824
+ const functionBtn = container.querySelector('.function-btn');
825
+ const objectBtn = container.querySelector('.object-btn');
826
+ const functionCountDiv = container.querySelector('.function-count');
827
+ const objectCountDiv = container.querySelector('.object-count');
828
+
829
+ expect(functionCountDiv.textContent).toBe('0');
830
+ expect(objectCountDiv.textContent).toBe('0');
831
+
832
+ functionBtn.click();
833
+ flushSync();
834
+ expect(functionCountDiv.textContent).toBe('1');
835
+
836
+ objectBtn.click();
837
+ flushSync();
838
+ expect(objectCountDiv.textContent).toBe('1');
839
+
840
+ // Test multiple clicks
841
+ functionBtn.click();
842
+ functionBtn.click();
843
+ flushSync();
844
+ expect(functionCountDiv.textContent).toBe('3');
845
+
846
+ objectBtn.click();
847
+ objectBtn.click();
848
+ flushSync();
849
+ expect(objectCountDiv.textContent).toBe('3');
850
+ });
851
+
852
+ it('handles passive event option', () => {
853
+ component Basic() {
854
+ let passiveDefaultPrevented = track<boolean | null>(null);
855
+ let nonPassiveDefaultPrevented = track<boolean | null>(null);
856
+
857
+ const passiveHandler = {
858
+ handleEvent(event: Event) {
859
+ event.preventDefault();
860
+ // In passive listeners, preventDefault() is ignored
861
+ @passiveDefaultPrevented = event.defaultPrevented;
862
+ },
863
+ passive: true,
864
+ delegated: false, // Need to ensure it's not delegated to test passive properly
865
+ };
866
+
867
+ const nonPassiveHandler = {
868
+ handleEvent(event: Event) {
869
+ event.preventDefault();
870
+ // In non-passive listeners, preventDefault() works
871
+ @nonPassiveDefaultPrevented = event.defaultPrevented;
872
+ },
873
+ delegated: false,
874
+ };
875
+
876
+ <div>
877
+ <div onWheel={passiveHandler} class="passive-target">{'Passive'}</div>
878
+ <div onWheel={nonPassiveHandler} class="non-passive-target">{'Non-Passive'}</div>
879
+ <div class="passive-result">
880
+ {@passiveDefaultPrevented === null
881
+ ? 'not-tested'
882
+ : @passiveDefaultPrevented
883
+ ? 'prevented'
884
+ : 'not-prevented'}
885
+ </div>
886
+ <div class="non-passive-result">
887
+ {@nonPassiveDefaultPrevented === null
888
+ ? 'not-tested'
889
+ : @nonPassiveDefaultPrevented
890
+ ? 'prevented'
891
+ : 'not-prevented'}
892
+ </div>
893
+ </div>
894
+ }
895
+
896
+ render(Basic);
897
+
898
+ const passiveTarget = container.querySelector('.passive-target');
899
+ const nonPassiveTarget = container.querySelector('.non-passive-target');
900
+ const passiveResultDiv = container.querySelector('.passive-result');
901
+ const nonPassiveResultDiv = container.querySelector('.non-passive-result');
902
+
903
+ expect(passiveResultDiv.textContent).toBe('not-tested');
904
+ expect(nonPassiveResultDiv.textContent).toBe('not-tested');
905
+
906
+ // Test passive event - preventDefault should be ignored, defaultPrevented stays false
907
+ passiveTarget.dispatchEvent(new WheelEvent('wheel', { bubbles: true, cancelable: true }));
908
+ flushSync();
909
+ expect(passiveResultDiv.textContent).toBe('not-prevented');
910
+
911
+ // Test non-passive event - preventDefault should work, defaultPrevented becomes true
912
+ nonPassiveTarget.dispatchEvent(new WheelEvent('wheel', { bubbles: true, cancelable: true }));
913
+ flushSync();
914
+ expect(nonPassiveResultDiv.textContent).toBe('prevented');
915
+ });
916
+
917
+ it('handles once option to fire event only once', () => {
918
+ component Basic() {
919
+ let onceCount = track(0);
920
+ let regularCount = track(0);
921
+
922
+ const onceHandler = {
923
+ handleEvent() {
924
+ @onceCount++;
925
+ },
926
+ once: true,
927
+ };
928
+
929
+ const regularHandler = () => {
930
+ @regularCount++;
931
+ };
932
+
933
+ <div>
934
+ <button onClick={onceHandler} class="once-btn">{'Once'}</button>
935
+ <button onClick={regularHandler} class="regular-btn">{'Regular'}</button>
936
+ <div class="once-count">{@onceCount}</div>
937
+ <div class="regular-count">{@regularCount}</div>
938
+ </div>
939
+ }
940
+
941
+ render(Basic);
942
+
943
+ const onceBtn = container.querySelector('.once-btn');
944
+ const regularBtn = container.querySelector('.regular-btn');
945
+ const onceCountDiv = container.querySelector('.once-count');
946
+ const regularCountDiv = container.querySelector('.regular-count');
947
+
948
+ expect(onceCountDiv.textContent).toBe('0');
949
+ expect(regularCountDiv.textContent).toBe('0');
950
+
951
+ onceBtn.click();
952
+ flushSync();
953
+ expect(onceCountDiv.textContent).toBe('1');
954
+
955
+ // Second click should not increment because of once: true
956
+ onceBtn.click();
957
+ flushSync();
958
+ expect(onceCountDiv.textContent).toBe('1');
959
+
960
+ // Regular handler should work multiple times
961
+ regularBtn.click();
962
+ flushSync();
963
+ expect(regularCountDiv.textContent).toBe('1');
964
+
965
+ regularBtn.click();
966
+ flushSync();
967
+ expect(regularCountDiv.textContent).toBe('2');
968
+ });
527
969
  });
@@ -42,9 +42,9 @@ describe('basic client > events', () => {
42
42
  let bubbleClicks = track(0);
43
43
 
44
44
  <div
45
- onClickCapture={() => {
45
+ onClick={{handleEvent: () => {
46
46
  @captureClicks++;
47
- }}
47
+ }, capture: true}}
48
48
  >
49
49
  <button
50
50
  onClick={() => {
@@ -1,3 +1,4 @@
1
+ import type { Tracked } from 'ripple';
1
2
  import { track, trackSplit, flushSync, effect } from 'ripple';
2
3
 
3
4
  describe('composite > reactivity', () => {
@@ -277,7 +278,7 @@ describe('composite > reactivity', () => {
277
278
  const more = #{
278
279
  double: track(() => props.@count * 2),
279
280
  another: track(0),
280
- onemore: 100,
281
+ extra: 100,
281
282
  };
282
283
 
283
284
  effect(() => {
@@ -290,7 +291,7 @@ describe('composite > reactivity', () => {
290
291
  });
291
292
 
292
293
  <div>
293
- <Counter {...props} double={more.double} another={more.another} onemore={more.onemore} />
294
+ <Counter {...props} double={more.double} another={more.another} extra={more.extra} />
294
295
  </div>
295
296
  }
296
297
 
@@ -307,7 +308,7 @@ describe('composite > reactivity', () => {
307
308
 
308
309
  expect(div.getAttribute('double')).toBe('0');
309
310
  expect(div.getAttribute('another')).toBe('0');
310
- expect(div.getAttribute('onemore')).toBe('100');
311
+ expect(div.getAttribute('extra')).toBe('100');
311
312
  expect(div.textContent).toBe('Counter: 0 Double: 0');
312
313
 
313
314
  buttonIncrement.click();
@@ -315,7 +316,7 @@ describe('composite > reactivity', () => {
315
316
 
316
317
  expect(div.getAttribute('double')).toBe('2');
317
318
  expect(div.hasAttribute('another')).toBe(false);
318
- expect(div.getAttribute('onemore')).toBe('100');
319
+ expect(div.getAttribute('extra')).toBe('100');
319
320
  expect(div.textContent).toBe('Counter: 1 Double: 2');
320
321
 
321
322
  buttonIncrement.click();
@@ -323,7 +324,7 @@ describe('composite > reactivity', () => {
323
324
 
324
325
  expect(div.getAttribute('double')).toBe('4');
325
326
  expect(div.getAttribute('another')).toBe('0');
326
- expect(div.getAttribute('onemore')).toBe('100');
327
+ expect(div.getAttribute('extra')).toBe('100');
327
328
  expect(div.textContent).toBe('Counter: 2 Double: 4');
328
329
  });
329
330
  });