ripple 0.2.115 → 0.2.118

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.
Files changed (93) hide show
  1. package/package.json +16 -16
  2. package/src/compiler/index.js +20 -1
  3. package/src/compiler/phases/1-parse/index.js +79 -0
  4. package/src/compiler/phases/3-transform/client/index.js +54 -8
  5. package/src/compiler/phases/3-transform/segments.js +107 -60
  6. package/src/compiler/phases/3-transform/server/index.js +21 -11
  7. package/src/compiler/types/index.d.ts +16 -0
  8. package/src/runtime/index-client.js +19 -185
  9. package/src/runtime/index-server.js +24 -0
  10. package/src/runtime/internal/client/bindings.js +443 -0
  11. package/src/runtime/internal/client/index.js +4 -0
  12. package/src/runtime/internal/client/runtime.js +10 -0
  13. package/src/runtime/internal/client/utils.js +0 -8
  14. package/src/runtime/map.js +11 -1
  15. package/src/runtime/set.js +11 -1
  16. package/tests/client/__snapshots__/for.test.ripple.snap +80 -0
  17. package/tests/client/_etc.test.ripple +5 -0
  18. package/tests/client/array/array.copy-within.test.ripple +120 -0
  19. package/tests/client/array/array.derived.test.ripple +495 -0
  20. package/tests/client/array/array.iteration.test.ripple +115 -0
  21. package/tests/client/array/array.mutations.test.ripple +385 -0
  22. package/tests/client/array/array.static.test.ripple +237 -0
  23. package/tests/client/array/array.to-methods.test.ripple +93 -0
  24. package/tests/client/basic/__snapshots__/basic.attributes.test.ripple.snap +60 -0
  25. package/tests/client/basic/__snapshots__/basic.rendering.test.ripple.snap +106 -0
  26. package/tests/client/basic/__snapshots__/basic.text.test.ripple.snap +49 -0
  27. package/tests/client/basic/basic.attributes.test.ripple +474 -0
  28. package/tests/client/basic/basic.collections.test.ripple +94 -0
  29. package/tests/client/basic/basic.components.test.ripple +225 -0
  30. package/tests/client/basic/basic.errors.test.ripple +126 -0
  31. package/tests/client/basic/basic.events.test.ripple +222 -0
  32. package/tests/client/basic/basic.reactivity.test.ripple +476 -0
  33. package/tests/client/basic/basic.rendering.test.ripple +204 -0
  34. package/tests/client/basic/basic.styling.test.ripple +63 -0
  35. package/tests/client/basic/basic.utilities.test.ripple +25 -0
  36. package/tests/client/boundaries.test.ripple +2 -21
  37. package/tests/client/compiler/__snapshots__/compiler.assignments.test.ripple.snap +12 -0
  38. package/tests/client/compiler/__snapshots__/compiler.typescript.test.ripple.snap +22 -0
  39. package/tests/client/compiler/compiler.assignments.test.ripple +112 -0
  40. package/tests/client/compiler/compiler.attributes.test.ripple +95 -0
  41. package/tests/client/compiler/compiler.basic.test.ripple +203 -0
  42. package/tests/client/compiler/compiler.regex.test.ripple +87 -0
  43. package/tests/client/compiler/compiler.typescript.test.ripple +29 -0
  44. package/tests/client/{__snapshots__/composite.test.ripple.snap → composite/__snapshots__/composite.render.test.ripple.snap} +2 -2
  45. package/tests/client/composite/composite.dynamic-components.test.ripple +100 -0
  46. package/tests/client/composite/composite.generics.test.ripple +211 -0
  47. package/tests/client/composite/composite.props.test.ripple +106 -0
  48. package/tests/client/composite/composite.reactivity.test.ripple +184 -0
  49. package/tests/client/composite/composite.render.test.ripple +84 -0
  50. package/tests/client/computed-properties.test.ripple +2 -21
  51. package/tests/client/context.test.ripple +5 -22
  52. package/tests/client/date.test.ripple +1 -20
  53. package/tests/client/dynamic-elements.test.ripple +16 -24
  54. package/tests/client/for.test.ripple +4 -23
  55. package/tests/client/head.test.ripple +11 -23
  56. package/tests/client/html.test.ripple +1 -20
  57. package/tests/client/input-value.test.ripple +11 -31
  58. package/tests/client/map.test.ripple +82 -20
  59. package/tests/client/media-query.test.ripple +10 -23
  60. package/tests/client/object.test.ripple +5 -24
  61. package/tests/client/portal.test.ripple +2 -19
  62. package/tests/client/ref.test.ripple +8 -26
  63. package/tests/client/set.test.ripple +84 -22
  64. package/tests/client/svg.test.ripple +1 -22
  65. package/tests/client/switch.test.ripple +6 -25
  66. package/tests/client/tracked-expression.test.ripple +2 -21
  67. package/tests/client/typescript-generics.test.ripple +0 -21
  68. package/tests/client/url/url.derived.test.ripple +83 -0
  69. package/tests/client/url/url.parsing.test.ripple +165 -0
  70. package/tests/client/url/url.partial-removal.test.ripple +198 -0
  71. package/tests/client/url/url.reactivity.test.ripple +449 -0
  72. package/tests/client/url/url.serialization.test.ripple +50 -0
  73. package/tests/client/url-search-params/url-search-params.derived.test.ripple +84 -0
  74. package/tests/client/url-search-params/url-search-params.initialization.test.ripple +61 -0
  75. package/tests/client/url-search-params/url-search-params.iteration.test.ripple +153 -0
  76. package/tests/client/url-search-params/url-search-params.mutation.test.ripple +343 -0
  77. package/tests/client/url-search-params/url-search-params.retrieval.test.ripple +160 -0
  78. package/tests/client/url-search-params/url-search-params.serialization.test.ripple +53 -0
  79. package/tests/client/url-search-params/url-search-params.tracked-url.test.ripple +55 -0
  80. package/tests/client.d.ts +12 -0
  81. package/tests/server/if.test.ripple +66 -0
  82. package/tests/setup-client.js +28 -0
  83. package/tsconfig.json +4 -2
  84. package/types/index.d.ts +92 -46
  85. package/LICENSE +0 -21
  86. package/tests/client/__snapshots__/basic.test.ripple.snap +0 -117
  87. package/tests/client/__snapshots__/compiler.test.ripple.snap +0 -33
  88. package/tests/client/array.test.ripple +0 -1455
  89. package/tests/client/basic.test.ripple +0 -1892
  90. package/tests/client/compiler.test.ripple +0 -541
  91. package/tests/client/composite.test.ripple +0 -692
  92. package/tests/client/url-search-params.test.ripple +0 -912
  93. package/tests/client/url.test.ripple +0 -954
@@ -0,0 +1,225 @@
1
+ import { track, flushSync } from 'ripple';
2
+
3
+ describe('basic client > components & composition', () => {
4
+ it('renders with component composition and children', () => {
5
+ component Card(props) {
6
+ <div class='card'>
7
+ <props.children />
8
+ </div>
9
+ }
10
+
11
+ component Basic() {
12
+ <Card>
13
+ component children() {
14
+ <p>{'Card content here'}</p>
15
+ }
16
+ </Card>
17
+ }
18
+
19
+ render(Basic);
20
+
21
+ const card = container.querySelector('.card');
22
+ const paragraph = card.querySelector('p');
23
+
24
+ expect(card).toBeTruthy();
25
+ expect(paragraph.textContent).toBe('Card content here');
26
+ });
27
+
28
+ it('renders with nested components and prop passing', () => {
29
+ component Button(props) {
30
+ <button class={props.variant} onClick={props.onClick}>
31
+ {props.label}
32
+ </button>
33
+ }
34
+
35
+ component Card(props) {
36
+ <div class='card'>
37
+ <h3>{props.title}</h3>
38
+ <p>{props.content}</p>
39
+ <Button variant='primary' label={props.buttonText} onClick={props.onAction} />
40
+ </div>
41
+ }
42
+
43
+ component Basic() {
44
+ let clicked = track(false);
45
+
46
+ <Card
47
+ title='Test Card'
48
+ content='This is a test card'
49
+ buttonText='Click me'
50
+ onAction={() => @clicked = true}
51
+ />
52
+ <div class='status'>{@clicked ? 'Clicked' : 'Not clicked'}</div>
53
+ }
54
+
55
+ render(Basic);
56
+
57
+ const card = container.querySelector('.card');
58
+ const title = card.querySelector('h3');
59
+ const content = card.querySelector('p');
60
+ const button = card.querySelector('button');
61
+ const status = container.querySelector('.status');
62
+
63
+ expect(title.textContent).toBe('Test Card');
64
+ expect(content.textContent).toBe('This is a test card');
65
+ expect(button.textContent).toBe('Click me');
66
+ expect(button.className).toBe('primary');
67
+ expect(status.textContent).toBe('Not clicked');
68
+
69
+ button.click();
70
+ flushSync();
71
+
72
+ expect(status.textContent).toBe('Clicked');
73
+ });
74
+
75
+ it('renders with reactive component props', () => {
76
+ component ChildComponent(props) {
77
+ <div class='child-content'>{props.@text}</div>
78
+ <div class='child-count'>{props.@count}</div>
79
+ }
80
+
81
+ component Basic() {
82
+ let message = track('Hello');
83
+ let number = track(1);
84
+
85
+ <ChildComponent text={message} count={number} />
86
+ <button onClick={() => {
87
+ @message = @message === 'Hello' ? 'Goodbye' : 'Hello';
88
+ @number++;
89
+ }}>{'Update Props'}</button>
90
+ }
91
+
92
+ render(Basic);
93
+
94
+ const contentDiv = container.querySelector('.child-content');
95
+ const countDiv = container.querySelector('.child-count');
96
+ const button = container.querySelector('button');
97
+
98
+ expect(contentDiv.textContent).toBe('Hello');
99
+ expect(countDiv.textContent).toBe('1');
100
+
101
+ button.click();
102
+ flushSync();
103
+
104
+ expect(contentDiv.textContent).toBe('Goodbye');
105
+ expect(countDiv.textContent).toBe('2');
106
+
107
+ button.click();
108
+ flushSync();
109
+
110
+ expect(contentDiv.textContent).toBe('Hello');
111
+ expect(countDiv.textContent).toBe('3');
112
+ });
113
+
114
+ it('it retains this context with bracketed prop functions and keeps original chaining', () => {
115
+ component App() {
116
+ const SYMBOL_PROP = Symbol();
117
+ let hasError = track(false);
118
+ const obj = {
119
+ count: track(0),
120
+ increment() {
121
+ this.@count++;
122
+ },
123
+ [SYMBOL_PROP]() {
124
+ this.@count++;
125
+ },
126
+ arr: [() => obj.@count++, () => obj.@count--],
127
+ };
128
+
129
+ const obj2 = null;
130
+
131
+ <button onClick={() => obj['increment']()}>{'Increment'}</button>
132
+ <button onClick={() => obj[SYMBOL_PROP]()}>{'Increment'}</button>
133
+ <button
134
+ onClick={() => {
135
+ @hasError = false;
136
+ try {
137
+ obj['nonexistent']();
138
+ } catch {
139
+ @hasError = true;
140
+ }
141
+ }}
142
+ >{'Nonexistent'}</button>
143
+ <button
144
+ onClick={() => {
145
+ @hasError = false;
146
+ try {
147
+ obj['nonexistent']?.();
148
+ } catch {
149
+ @hasError = true;
150
+ }
151
+ }}
152
+ >{'Nonexistent chaining'}</button>
153
+ <button
154
+ onClick={() => {
155
+ @hasError = false;
156
+ try {
157
+ obj2['nonexistent']();
158
+ } catch {
159
+ @hasError = true;
160
+ }
161
+ }}
162
+ >{'Object null'}</button>
163
+ <button
164
+ onClick={() => {
165
+ @hasError = false;
166
+ try {
167
+ obj2?.['nonexistent']?.();
168
+ } catch {
169
+ @hasError = true;
170
+ }
171
+ }}
172
+ >{'Object null chained'}</button>
173
+ <button onClick={() => obj.arr[obj.arr.length - 1]()}>{'BinaryExpression prop'}</button>
174
+
175
+ <span>{obj.@count}</span>
176
+ <span>{@hasError}</span>
177
+ }
178
+
179
+ render(App);
180
+
181
+ const button1 = container.querySelectorAll('button')[0];
182
+ const button2 = container.querySelectorAll('button')[1];
183
+ const button3 = container.querySelectorAll('button')[2];
184
+ const button4 = container.querySelectorAll('button')[3];
185
+ const button5 = container.querySelectorAll('button')[4];
186
+ const button6 = container.querySelectorAll('button')[5];
187
+ const button7 = container.querySelectorAll('button')[6];
188
+
189
+ const countSpan = container.querySelectorAll('span')[0];
190
+ const errorSpan = container.querySelectorAll('span')[1];
191
+
192
+ expect(countSpan.textContent).toBe('0');
193
+ expect(errorSpan.textContent).toBe('false');
194
+
195
+ button1.click();
196
+ flushSync();
197
+
198
+ expect(countSpan.textContent).toBe('1');
199
+
200
+ button2.click();
201
+ flushSync();
202
+
203
+ expect(countSpan.textContent).toBe('2');
204
+
205
+ button3.click();
206
+ flushSync();
207
+ expect(errorSpan.textContent).toBe('true');
208
+
209
+ button4.click();
210
+ flushSync();
211
+ expect(errorSpan.textContent).toBe('false');
212
+
213
+ button5.click();
214
+ flushSync();
215
+ expect(errorSpan.textContent).toBe('true');
216
+
217
+ button6.click();
218
+ flushSync();
219
+ expect(errorSpan.textContent).toBe('false');
220
+
221
+ button7.click();
222
+ flushSync();
223
+ expect(countSpan.textContent).toBe('1');
224
+ });
225
+ });
@@ -0,0 +1,126 @@
1
+ import { track, flushSync, untrack } from 'ripple';
2
+ import { compile } from 'ripple/compiler';
3
+
4
+ describe('basic client > errors', () => {
5
+ it('renders with error handling simulation', () => {
6
+ component Basic() {
7
+ let hasError = track(false);
8
+ let errorMessage = track('');
9
+
10
+ const triggerError = () => {
11
+ try {
12
+ throw new Error('Test error');
13
+ } catch (e) {
14
+ @hasError = true;
15
+ @errorMessage = (e as Error).message;
16
+ }
17
+ };
18
+
19
+ <div>
20
+ <button onClick={triggerError}>{'Trigger Error'}</button>
21
+ if (@hasError) {
22
+ <div class='error'>{'Error caught: ' + @errorMessage}</div>
23
+ } else {
24
+ <div class='success'>{'No error'}</div>
25
+ }
26
+ </div>
27
+ }
28
+
29
+ render(Basic);
30
+
31
+ const button = container.querySelector('button');
32
+ const successDiv = container.querySelector('.success');
33
+
34
+ expect(successDiv).toBeTruthy();
35
+ expect(successDiv.textContent).toBe('No error');
36
+
37
+ button.click();
38
+ flushSync();
39
+
40
+ const errorDiv = container.querySelector('.error');
41
+ expect(errorDiv).toBeTruthy();
42
+ expect(errorDiv.textContent).toBe('Error caught: Test error');
43
+ });
44
+
45
+ it('should throw error for unclosed tag', () => {
46
+ const malformedCode = `export default component Example() {
47
+ <div></span>
48
+ }`;
49
+ expect(() => {
50
+ compile(malformedCode, 'test.ripple');
51
+ }).toThrow('Expected closing tag to match opening tag');
52
+ });
53
+
54
+ it('should throw error for completely unclosed tag', () => {
55
+ const malformedCode = `export default component Example() {
56
+ <div>content
57
+ }`;
58
+
59
+ expect(() => {
60
+ compile(malformedCode, 'test.ripple');
61
+ }).toThrow('Unclosed tag');
62
+ });
63
+
64
+ it('errors on mutating tracked value inside computed track() evaluation', () => {
65
+ component Basic() {
66
+ let count = track(0);
67
+
68
+ const doubled = track(() => {
69
+ try {
70
+ @count *= 2;
71
+ } catch (e) {
72
+ error = (e as Error).message;
73
+ }
74
+ });
75
+
76
+ <p>{@doubled}</p>
77
+ }
78
+
79
+ render(Basic);
80
+
81
+ expect(error).toBe('Assignments or updates to tracked values are not allowed during computed "track(() => ...)" evaluation');
82
+ });
83
+
84
+ it('errors on mutating tracked value inside untrack() in computed track() evaluation', () => {
85
+ component Basic() {
86
+ let count = track(0);
87
+
88
+ const doubled = track(() => {
89
+ try {
90
+ untrack(() => {
91
+ @count *= 2;
92
+ });
93
+ } catch (e) {
94
+ error = (e as Error).message;
95
+ }
96
+ });
97
+
98
+ <p>{@doubled}</p>
99
+ }
100
+
101
+ render(Basic);
102
+
103
+ expect(error).toBe('Assignments or updates to tracked values are not allowed during computed "track(() => ...)" evaluation');
104
+ });
105
+
106
+ it("errors on mutating a tracked variable in track() getter", () => {
107
+ component Basic() {
108
+ let count = track(0);
109
+
110
+ const doubled = track(0, (value) => {
111
+ try {
112
+ @count += 1;
113
+ } catch (e) {
114
+ error = (e as Error).message;
115
+ }
116
+ return value;
117
+ });
118
+
119
+ <p>{@doubled}</p>
120
+ }
121
+
122
+ render(Basic);
123
+
124
+ expect(error).toBe('Assignments or updates to tracked values are not allowed during computed "track(() => ...)" evaluation');
125
+ });
126
+ });
@@ -0,0 +1,222 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { track, flushSync } from 'ripple';
3
+
4
+ describe('basic client > events', () => {
5
+ it('renders with different event types', () => {
6
+ component Basic() {
7
+ let focusCount = track(0);
8
+ let clickCount = track(0);
9
+
10
+ <button
11
+ onFocus={() => { @focusCount++ }}
12
+ onClick={() => { @clickCount++ }}
13
+ >{'Test Button'}</button>
14
+ <div class='focus-count'>{@focusCount}</div>
15
+ <div class='click-count'>{@clickCount}</div>
16
+ }
17
+
18
+ render(Basic);
19
+
20
+ const button = container.querySelector('button');
21
+ const focusDiv = container.querySelector('.focus-count');
22
+ const clickDiv = container.querySelector('.click-count');
23
+
24
+ button.dispatchEvent(new Event('focus'));
25
+ flushSync();
26
+ expect(focusDiv.textContent).toBe('1');
27
+
28
+ button.click();
29
+ flushSync();
30
+ expect(clickDiv.textContent).toBe('1');
31
+ });
32
+
33
+ it('renders with capture events', () => {
34
+ component Basic() {
35
+ let captureClicks = track(0);
36
+ let bubbleClicks = track(0);
37
+
38
+ <div onClickCapture={() => { @captureClicks++ }}>
39
+ <button onClick={() => { @bubbleClicks++ }}>{'Click me'}</button>
40
+ <div class='capture-count'>{@captureClicks}</div>
41
+ <div class='bubble-count'>{@bubbleClicks}</div>
42
+ </div>
43
+ }
44
+
45
+ render(Basic);
46
+
47
+ const button = container.querySelector('button');
48
+ const captureDiv = container.querySelector('.capture-count');
49
+ const bubbleDiv = container.querySelector('.bubble-count');
50
+
51
+ button.click();
52
+ flushSync();
53
+
54
+ expect(captureDiv.textContent).toBe('1');
55
+ expect(bubbleDiv.textContent).toBe('1');
56
+ });
57
+
58
+ it('renders with event listeners in spread props', () => {
59
+ component Basic() {
60
+ let count = track(0);
61
+
62
+ const minus = {
63
+ onClick() {
64
+ @count--
65
+ }
66
+ }
67
+
68
+ const plus = {
69
+ onClick() {
70
+ @count++
71
+ }
72
+ }
73
+
74
+ <div>
75
+ <button {...minus} class='minus'>{'-'}</button>
76
+ <span class='count'>{@count}</span>
77
+ <button {...plus} class='plus'>{'+'}</button>
78
+ </div>
79
+ }
80
+
81
+ render(Basic);
82
+
83
+ const minusButton = container.querySelector('.minus');
84
+ const plusButton = container.querySelector('.plus');
85
+ const countSpan = container.querySelector('.count');
86
+
87
+ expect(countSpan.textContent).toBe('0');
88
+
89
+ // Test that the buttons don't have string onclick attributes
90
+ expect(minusButton.getAttribute('onclick')).toBe(null);
91
+ expect(plusButton.getAttribute('onclick')).toBe(null);
92
+
93
+ // Test that the event handlers work
94
+ minusButton.click();
95
+ flushSync();
96
+ expect(countSpan.textContent).toBe('-1');
97
+
98
+ plusButton.click();
99
+ flushSync();
100
+ expect(countSpan.textContent).toBe('0');
101
+
102
+ plusButton.click();
103
+ flushSync();
104
+ expect(countSpan.textContent).toBe('1');
105
+ });
106
+
107
+ it('handles both delegated and non-delegated events in spread props', () => {
108
+ component Basic() {
109
+ let clickCount = track(0);
110
+ let focusCount = track(0);
111
+
112
+ const mixedHandler = {
113
+ onClick() { // Delegated event
114
+ @clickCount++
115
+ },
116
+ onFocus() { // Non-delegated event
117
+ @focusCount++
118
+ }
119
+ }
120
+
121
+ <div>
122
+ <button {...mixedHandler} class='mixed-button'>{'Test'}</button>
123
+ <span class='click-count'>{@clickCount}</span>
124
+ <span class='focus-count'>{@focusCount}</span>
125
+ </div>
126
+ }
127
+
128
+ render(Basic);
129
+
130
+ const button = container.querySelector('.mixed-button');
131
+ const clickSpan = container.querySelector('.click-count');
132
+ const focusSpan = container.querySelector('.focus-count');
133
+
134
+ expect(clickSpan.textContent).toBe('0');
135
+ expect(focusSpan.textContent).toBe('0');
136
+
137
+ // Test delegated event (click)
138
+ button.click();
139
+ flushSync();
140
+ expect(clickSpan.textContent).toBe('1');
141
+
142
+ // Test non-delegated event (focus)
143
+ button.dispatchEvent(new Event('focus'));
144
+ flushSync();
145
+ expect(focusSpan.textContent).toBe('1');
146
+ });
147
+
148
+ it('renders with complex event handling and state updates', () => {
149
+ component Basic() {
150
+ let counter = track(0);
151
+ let history = track<string[]>([]);
152
+ let isEven = track(true);
153
+
154
+ const handleIncrement = () => {
155
+ @counter++;
156
+ @history = [...@history, `Inc to ${@counter}`];
157
+ @isEven = @counter % 2 === 0;
158
+ };
159
+
160
+ const handleDecrement = () => {
161
+ @counter--;
162
+ @history = [...@history, `Dec to ${@counter}`];
163
+ @isEven = @counter % 2 === 0;
164
+ };
165
+
166
+ const handleReset = () => {
167
+ @counter = 0;
168
+ @history = [...@history, 'Reset'];
169
+ @isEven = true;
170
+ };
171
+
172
+ <div class='counter'>{@counter}</div>
173
+ <div class='parity'>{@isEven ? 'Even' : 'Odd'}</div>
174
+ <div class='history-count'>{@history.length}</div>
175
+
176
+ <button class='inc-btn' onClick={handleIncrement}>{'+'}</button>
177
+ <button class='dec-btn' onClick={handleDecrement}>{'-'}</button>
178
+ <button class='reset-btn' onClick={handleReset}>{'Reset'}</button>
179
+ }
180
+
181
+ render(Basic);
182
+
183
+ const counterDiv = container.querySelector('.counter');
184
+ const parityDiv = container.querySelector('.parity');
185
+ const historyDiv = container.querySelector('.history-count');
186
+ const incBtn = container.querySelector('.inc-btn');
187
+ const decBtn = container.querySelector('.dec-btn');
188
+ const resetBtn = container.querySelector('.reset-btn');
189
+
190
+ expect(counterDiv.textContent).toBe('0');
191
+ expect(parityDiv.textContent).toBe('Even');
192
+ expect(historyDiv.textContent).toBe('0');
193
+
194
+ incBtn.click();
195
+ flushSync();
196
+
197
+ expect(counterDiv.textContent).toBe('1');
198
+ expect(parityDiv.textContent).toBe('Odd');
199
+ expect(historyDiv.textContent).toBe('1');
200
+
201
+ incBtn.click();
202
+ flushSync();
203
+
204
+ expect(counterDiv.textContent).toBe('2');
205
+ expect(parityDiv.textContent).toBe('Even');
206
+ expect(historyDiv.textContent).toBe('2');
207
+
208
+ decBtn.click();
209
+ flushSync();
210
+
211
+ expect(counterDiv.textContent).toBe('1');
212
+ expect(parityDiv.textContent).toBe('Odd');
213
+ expect(historyDiv.textContent).toBe('3');
214
+
215
+ resetBtn.click();
216
+ flushSync();
217
+
218
+ expect(counterDiv.textContent).toBe('0');
219
+ expect(parityDiv.textContent).toBe('Even');
220
+ expect(historyDiv.textContent).toBe('4');
221
+ });
222
+ });