ripple 0.2.90 → 0.2.92

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,6 +1,6 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
2
  import { mount, TrackedArray, track } from 'ripple';
3
- import { parse } from 'ripple/compiler'
3
+ import { parse, compile } from 'ripple/compiler'
4
4
 
5
5
  describe('compiler success tests', () => {
6
6
  let container;
@@ -280,4 +280,98 @@ describe('compiler success tests', () => {
280
280
 
281
281
  render(App);
282
282
  });
283
+
284
+ describe('attribute name handling', () => {
285
+ it('generates valid JavaScript for component props with hyphenated attributes', () => {
286
+ const source = `
287
+ component Child(props) {
288
+ <div />
289
+ }
290
+
291
+ export default component App() {
292
+ <Child data-scope="test" aria-label="accessible" class="valid" />
293
+ }`;
294
+
295
+ const result = compile(source, 'test.ripple', { mode: 'client' });
296
+
297
+ // Should contain properly quoted hyphenated properties and unquoted valid identifiers
298
+ expect(result.js.code).toMatch(/'data-scope': "test"/);
299
+ expect(result.js.code).toMatch(/'aria-label': "accessible"/);
300
+ expect(result.js.code).toMatch(/class: "valid"/);
301
+ });
302
+
303
+ it('generates valid JavaScript for all types of hyphenated attributes', () => {
304
+ const testCases = [
305
+ { attr: 'data-testid="value"', expected: /'data-testid': "value"/ },
306
+ { attr: 'aria-label="label"', expected: /'aria-label': "label"/ },
307
+ { attr: 'data-custom-attr="custom"', expected: /'data-custom-attr': "custom"/ },
308
+ { attr: 'ng-if="condition"', expected: /'ng-if': "condition"/ },
309
+ ];
310
+
311
+ testCases.forEach(({ attr, expected }) => {
312
+ const source = `
313
+ component Child(props) { <div /> }
314
+ export default component App() { <Child ${attr} /> }`;
315
+
316
+ const result = compile(source, 'test.ripple', { mode: 'client' });
317
+ expect(result.js.code).toMatch(expected);
318
+ });
319
+ });
320
+
321
+ it('handles mixed valid and invalid attribute identifiers correctly', () => {
322
+ const source = `
323
+ component Child(props) {
324
+ <div />
325
+ }
326
+
327
+ export default component App() {
328
+ <Child
329
+ validProp="valid"
330
+ class="valid"
331
+ id="valid"
332
+ data-invalid="invalid"
333
+ aria-invalid="invalid"
334
+ custom-prop="invalid"
335
+ />
336
+ }`;
337
+
338
+ const result = compile(source, 'test.ripple', { mode: 'client' });
339
+
340
+ // Valid identifiers should not be quoted
341
+ expect(result.js.code).toMatch(/validProp: "valid"/);
342
+ expect(result.js.code).toMatch(/class: "valid"/);
343
+ expect(result.js.code).toMatch(/id: "valid"/);
344
+
345
+ // Invalid identifiers (with hyphens) should be quoted
346
+ expect(result.js.code).toMatch(/'data-invalid': "invalid"/);
347
+ expect(result.js.code).toMatch(/'aria-invalid': "invalid"/);
348
+ expect(result.js.code).toMatch(/'custom-prop': "invalid"/);
349
+ });
350
+
351
+ it('ensures generated code is syntactically valid JavaScript', () => {
352
+ const source = `
353
+ component Child(props) {
354
+ <div />
355
+ }
356
+
357
+ export default component App() {
358
+ <Child data-scope="test" />
359
+ }`;
360
+
361
+ const result = compile(source, 'test.ripple', { mode: 'client' });
362
+
363
+ // Extract the props object from the generated code and test it's valid JavaScript
364
+ const match = result.js.code.match(/Child\([^,]+,\s*(\{[^}]+\})/);
365
+ expect(match).toBeTruthy();
366
+
367
+ const propsObject = match[1];
368
+ expect(() => {
369
+ // Test that the object literal is syntactically valid
370
+ new Function(`return ${propsObject}`);
371
+ }).not.toThrow();
372
+
373
+ // Also verify it contains the expected quoted property
374
+ expect(propsObject).toMatch(/'data-scope': "test"/);
375
+ });
376
+ });
283
377
  });
@@ -0,0 +1,392 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mount, flushSync, TrackedDate, track } from 'ripple';
3
+
4
+ describe('TrackedDate', () => {
5
+ let container;
6
+
7
+ function render(component) {
8
+ mount(component, {
9
+ target: container,
10
+ });
11
+ }
12
+
13
+ beforeEach(() => {
14
+ container = document.createElement('div');
15
+ document.body.appendChild(container);
16
+ });
17
+
18
+ afterEach(() => {
19
+ document.body.removeChild(container);
20
+ container = null;
21
+ });
22
+
23
+ it('handles getTime() with reactive updates', () => {
24
+ component DateTest() {
25
+ let date = new TrackedDate(2025, 0, 1);
26
+ let time = track(() => date.getTime());
27
+
28
+ <button onClick={() => date.setFullYear(2026)}>{'Change Year'}</button>
29
+ <pre>{@time}</pre>
30
+ }
31
+
32
+ render(DateTest);
33
+
34
+ const button = container.querySelector('button');
35
+ const initialTime = container.querySelector('pre').textContent;
36
+
37
+ button.click();
38
+ flushSync();
39
+
40
+ const newTime = container.querySelector('pre').textContent;
41
+ expect(newTime).not.toBe(initialTime);
42
+ expect(parseInt(newTime)).toBeGreaterThan(parseInt(initialTime));
43
+ });
44
+
45
+ it('handles getFullYear() with reactive updates', () => {
46
+ component DateTest() {
47
+ let date = new TrackedDate(2025, 5, 15);
48
+ let year = track(() => date.getFullYear());
49
+
50
+ <button onClick={() => date.setFullYear(2030)}>{'Change Year'}</button>
51
+ <pre>{@year}</pre>
52
+ }
53
+
54
+ render(DateTest);
55
+
56
+ const button = container.querySelector('button');
57
+
58
+ expect(container.querySelector('pre').textContent).toBe('2025');
59
+
60
+ button.click();
61
+ flushSync();
62
+
63
+ expect(container.querySelector('pre').textContent).toBe('2030');
64
+ });
65
+
66
+ it('handles getMonth() with reactive updates', () => {
67
+ component DateTest() {
68
+ let date = new TrackedDate(2025, 0, 15);
69
+ let month = track(() => date.getMonth());
70
+
71
+ <button onClick={() => date.setMonth(11)}>{'Change to December'}</button>
72
+ <pre>{@month}</pre>
73
+ }
74
+
75
+ render(DateTest);
76
+
77
+ const button = container.querySelector('button');
78
+
79
+ expect(container.querySelector('pre').textContent).toBe('0');
80
+
81
+ button.click();
82
+ flushSync();
83
+
84
+ expect(container.querySelector('pre').textContent).toBe('11');
85
+ });
86
+
87
+ it('handles getDate() with reactive updates', () => {
88
+ component DateTest() {
89
+ let date = new TrackedDate(2025, 0, 1);
90
+ let day = track(() => date.getDate());
91
+
92
+ <button onClick={() => date.setDate(15)}>{'Change Day'}</button>
93
+ <pre>{@day}</pre>
94
+ }
95
+
96
+ render(DateTest);
97
+
98
+ const button = container.querySelector('button');
99
+
100
+ expect(container.querySelector('pre').textContent).toBe('1');
101
+
102
+ button.click();
103
+ flushSync();
104
+
105
+ expect(container.querySelector('pre').textContent).toBe('15');
106
+ });
107
+
108
+ it('handles getDay() with reactive updates', () => {
109
+ component DateTest() {
110
+ let date = new TrackedDate(2025, 0, 1);
111
+ let dayOfWeek = track(() => date.getDay());
112
+
113
+ <button onClick={() => date.setDate(2)}>{'Next Day'}</button>
114
+ <pre>{@dayOfWeek}</pre>
115
+ }
116
+
117
+ render(DateTest);
118
+
119
+ const button = container.querySelector('button');
120
+
121
+ expect(container.querySelector('pre').textContent).toBe('3');
122
+
123
+ button.click();
124
+ flushSync();
125
+
126
+ expect(container.querySelector('pre').textContent).toBe('4');
127
+ });
128
+
129
+ it('handles getHours() with reactive updates', () => {
130
+ component DateTest() {
131
+ let date = new TrackedDate(2025, 0, 1, 10, 30, 0);
132
+ let hours = track(() => date.getHours());
133
+
134
+ <button onClick={() => date.setHours(15)}>{'Change to 3 PM'}</button>
135
+ <pre>{@hours}</pre>
136
+ }
137
+
138
+ render(DateTest);
139
+
140
+ const button = container.querySelector('button');
141
+
142
+ expect(container.querySelector('pre').textContent).toBe('10');
143
+
144
+ button.click();
145
+ flushSync();
146
+
147
+ expect(container.querySelector('pre').textContent).toBe('15');
148
+ });
149
+
150
+ it('handles getMinutes() with reactive updates', () => {
151
+ component DateTest() {
152
+ let date = new TrackedDate(2025, 0, 1, 10, 15, 0);
153
+ let minutes = track(() => date.getMinutes());
154
+
155
+ <button onClick={() => date.setMinutes(45)}>{'Change Minutes'}</button>
156
+ <pre>{@minutes}</pre>
157
+ }
158
+
159
+ render(DateTest);
160
+
161
+ const button = container.querySelector('button');
162
+
163
+ expect(container.querySelector('pre').textContent).toBe('15');
164
+
165
+ button.click();
166
+ flushSync();
167
+
168
+ expect(container.querySelector('pre').textContent).toBe('45');
169
+ });
170
+
171
+ it('handles getSeconds() with reactive updates', () => {
172
+ component DateTest() {
173
+ let date = new TrackedDate(2025, 0, 1, 10, 15, 30);
174
+ let seconds = track(() => date.getSeconds());
175
+
176
+ <button onClick={() => date.setSeconds(45)}>{'Change Seconds'}</button>
177
+ <pre>{@seconds}</pre>
178
+ }
179
+
180
+ render(DateTest);
181
+
182
+ const button = container.querySelector('button');
183
+
184
+ expect(container.querySelector('pre').textContent).toBe('30');
185
+
186
+ button.click();
187
+ flushSync();
188
+
189
+ expect(container.querySelector('pre').textContent).toBe('45');
190
+ });
191
+
192
+ it('handles toISOString() with reactive updates', () => {
193
+ component DateTest() {
194
+ let date = new TrackedDate(2025, 0, 1, 12, 0, 0);
195
+ let isoString = track(() => date.toISOString());
196
+
197
+ <button onClick={() => date.setFullYear(2026)}>{'Change Year'}</button>
198
+ <pre>{@isoString}</pre>
199
+ }
200
+
201
+ render(DateTest);
202
+
203
+ const button = container.querySelector('button');
204
+ const initialISO = container.querySelector('pre').textContent;
205
+
206
+ expect(initialISO).toContain('2025');
207
+
208
+ button.click();
209
+ flushSync();
210
+
211
+ const newISO = container.querySelector('pre').textContent;
212
+
213
+ // Just verify that the ISO string changed after the year was updated
214
+ expect(newISO).not.toBe(initialISO);
215
+ expect(newISO.length).toBeGreaterThan(0);
216
+ });
217
+
218
+ it('handles toDateString() with reactive updates', () => {
219
+ component DateTest() {
220
+ let date = new TrackedDate(2025, 0, 1);
221
+ let dateString = track(() => date.toDateString());
222
+
223
+ <button onClick={() => date.setMonth(11)}>{'Change to December'}</button>
224
+ <pre>{@dateString}</pre>
225
+ }
226
+
227
+ render(DateTest);
228
+
229
+ const button = container.querySelector('button');
230
+ const initialDateString = container.querySelector('pre').textContent;
231
+
232
+ expect(initialDateString).toContain('Jan');
233
+
234
+ button.click();
235
+ flushSync();
236
+
237
+ const newDateString = container.querySelector('pre').textContent;
238
+ expect(newDateString).toContain('Dec');
239
+ expect(newDateString).not.toBe(initialDateString);
240
+ });
241
+
242
+ it('handles valueOf() with reactive updates', () => {
243
+ component DateTest() {
244
+ let date = new TrackedDate(2025, 0, 1);
245
+ let valueOf = track(() => date.valueOf());
246
+
247
+ <button onClick={() => date.setDate(2)}>{'Next Day'}</button>
248
+ <pre>{@valueOf}</pre>
249
+ }
250
+
251
+ render(DateTest);
252
+
253
+ const button = container.querySelector('button');
254
+ const initialValue = parseInt(container.querySelector('pre').textContent);
255
+
256
+ button.click();
257
+ flushSync();
258
+
259
+ const newValue = parseInt(container.querySelector('pre').textContent);
260
+ expect(newValue).toBeGreaterThan(initialValue);
261
+ expect(newValue - initialValue).toBe(24 * 60 * 60 * 1000);
262
+ });
263
+
264
+ it('handles multiple get methods reacting to same setTime change', () => {
265
+ component DateTest() {
266
+ let date = new TrackedDate(2025, 0, 1, 10, 30, 15);
267
+ let year = track(() => date.getFullYear());
268
+ let month = track(() => date.getMonth());
269
+ let day = track(() => date.getDate());
270
+ let hours = track(() => date.getHours());
271
+
272
+ <button onClick={() => date.setTime(new Date(2026, 5, 15, 14, 45, 30).getTime())}>{'Change All'}</button>
273
+ <div>
274
+ {'Year: '}
275
+ {@year}
276
+ </div>
277
+ <div>
278
+ {'Month: '}
279
+ {@month}
280
+ </div>
281
+ <div>
282
+ {'Day: '}
283
+ {@day}
284
+ </div>
285
+ <div>
286
+ {'Hours: '}
287
+ {@hours}
288
+ </div>
289
+ }
290
+
291
+ render(DateTest);
292
+
293
+ const button = container.querySelector('button');
294
+ const divs = container.querySelectorAll('div');
295
+
296
+ expect(divs[0].textContent).toBe('Year: 2025');
297
+ expect(divs[1].textContent).toBe('Month: 0');
298
+ expect(divs[2].textContent).toBe('Day: 1');
299
+ expect(divs[3].textContent).toBe('Hours: 10');
300
+
301
+ button.click();
302
+ flushSync();
303
+
304
+ expect(divs[0].textContent).toBe('Year: 2026');
305
+ expect(divs[1].textContent).toBe('Month: 5');
306
+ expect(divs[2].textContent).toBe('Day: 15');
307
+ expect(divs[3].textContent).toBe('Hours: 14');
308
+ });
309
+
310
+ it('handles constructor with different parameter combinations', () => {
311
+ component DateTest() {
312
+ let dateNow = new TrackedDate();
313
+ let dateFromString = new TrackedDate('2025-01-01');
314
+ let dateFromNumbers = new TrackedDate(2025, 0, 1);
315
+ let dateFromTimestamp = new TrackedDate(1735689600000);
316
+
317
+ let nowYear = track(() => dateNow.getFullYear());
318
+ let stringYear = track(() => dateFromString.getFullYear());
319
+ let numbersYear = track(() => dateFromNumbers.getFullYear());
320
+ let timestampYear = track(() => dateFromTimestamp.getFullYear());
321
+
322
+ <div>
323
+ {'Now: '}
324
+ {@nowYear}
325
+ </div>
326
+ <div>
327
+ {'String: '}
328
+ {@stringYear}
329
+ </div>
330
+ <div>
331
+ {'Numbers: '}
332
+ {@numbersYear}
333
+ </div>
334
+ <div>
335
+ {'Timestamp: '}
336
+ {@timestampYear}
337
+ </div>
338
+ }
339
+
340
+ render(DateTest);
341
+
342
+ const divs = container.querySelectorAll('div');
343
+ const currentYear = new Date().getFullYear();
344
+
345
+ expect(parseInt(divs[0].textContent.split(': ')[1])).toBe(currentYear);
346
+
347
+ // String date parsing may vary by timezone, just check it's a reasonable year
348
+ const stringYear = parseInt(divs[1].textContent.split(': ')[1]);
349
+ expect(stringYear).toBeGreaterThanOrEqual(2024);
350
+ expect(stringYear).toBeLessThanOrEqual(2025);
351
+ expect(divs[2].textContent).toBe('Numbers: 2025');
352
+
353
+ // Timestamp parsing may also vary by timezone
354
+ const timestampYear = parseInt(divs[3].textContent.split(': ')[1]);
355
+ expect(timestampYear).toBeGreaterThanOrEqual(2024);
356
+ expect(timestampYear).toBeLessThanOrEqual(2025);
357
+ });
358
+
359
+ it('handles get methods with arguments non-memoized', () => {
360
+ component DateTest() {
361
+ let date = new TrackedDate();
362
+ let localeDateString = track(() => date.toLocaleDateString('en-US'));
363
+ let localeTimeString = track(() => date.toLocaleTimeString('en-US'));
364
+
365
+ <button onClick={() => date.setFullYear(date.getFullYear() + 1)}>{'Next Year'}</button>
366
+ <div>
367
+ {'Date: '}
368
+ {@localeDateString}
369
+ </div>
370
+ <div>
371
+ {'Time: '}
372
+ {@localeTimeString}
373
+ </div>
374
+ }
375
+
376
+ render(DateTest);
377
+
378
+ const button = container.querySelector('button');
379
+ const divs = container.querySelectorAll('div');
380
+ const initialDate = divs[0].textContent;
381
+ const initialTime = divs[1].textContent;
382
+
383
+ button.click();
384
+ flushSync();
385
+
386
+ const newDate = divs[0].textContent;
387
+ const newTime = divs[1].textContent;
388
+
389
+ expect(newDate).not.toBe(initialDate);
390
+ expect(newTime).toBe(initialTime);
391
+ });
392
+ });
@@ -49,4 +49,32 @@ describe('html directive', () => {
49
49
 
50
50
  expect(container).toMatchSnapshot();
51
51
  });
52
- });
52
+
53
+ it('renders the correct namespace for child svg element when html is surrounded by <svg>', () => {
54
+ component App() {
55
+ let str = '<circle r="45" cx="50" cy="50" fill="red" />';
56
+
57
+ <svg height="100" width="100">{html str}</svg>
58
+ }
59
+
60
+ render(App);
61
+
62
+ const circle = container.querySelector('circle');
63
+ expect(circle.namespaceURI).toBe('http://www.w3.org/2000/svg');
64
+ });
65
+
66
+ it('renders the correct namespace for child math element when html is surrounded by <math>', () => {
67
+ component App() {
68
+ let str = '<mi>x</mi><mo>+</mo><mi>y</mi>';
69
+
70
+ <math>{html str}</math>;
71
+ }
72
+
73
+ render(App);
74
+
75
+ const mi = container.querySelector('mi');
76
+ const mo = container.querySelector('mo');
77
+ expect(mi.namespaceURI).toBe('http://www.w3.org/1998/Math/MathML');
78
+ expect(mo.namespaceURI).toBe('http://www.w3.org/1998/Math/MathML');
79
+ });
80
+ });
@@ -0,0 +1,167 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mount, Portal, track, flushSync } from 'ripple';
3
+
4
+ describe('Portal', () => {
5
+ let container;
6
+
7
+ function render(component) {
8
+ mount(component, {
9
+ target: container,
10
+ });
11
+ }
12
+
13
+ beforeEach(() => {
14
+ container = document.createElement('div');
15
+ document.body.appendChild(container);
16
+ });
17
+
18
+ afterEach(() => {
19
+ // Remove container
20
+ document.body.removeChild(container);
21
+
22
+ // Clean up any leftover portal content from document.body
23
+ const portals = document.body.querySelectorAll('.test-portal');
24
+ portals.forEach(el => el.remove());
25
+ });
26
+
27
+ it('renders portal content to target element', () => {
28
+ const target = document.createElement('div');
29
+ document.body.appendChild(target);
30
+
31
+ component TestPortal() {
32
+ <Portal target={target}><div class='test-portal'>{'Portal works!'}</div></Portal>
33
+ }
34
+
35
+ render(TestPortal);
36
+
37
+ // Portal content should be in the target, not in container
38
+ expect(container.querySelector('.test-portal')).toBeNull();
39
+ expect(target.querySelector('.test-portal')).toBeTruthy();
40
+ expect(target.querySelector('.test-portal').textContent).toBe('Portal works!');
41
+
42
+ document.body.removeChild(target);
43
+ });
44
+
45
+ it('renders portal content to document.body', () => {
46
+ component TestPortal() {
47
+ <Portal target={document.body}><div class='test-portal'>{'In document.body!'}</div></Portal>
48
+ }
49
+
50
+ render(TestPortal);
51
+
52
+ // Should not be in container
53
+ expect(container.querySelector('.test-portal')).toBeNull();
54
+
55
+ // Should be in document.body
56
+ expect(document.body.querySelector('.test-portal')).toBeTruthy();
57
+ expect(document.body.querySelector('.test-portal').textContent).toBe('In document.body!');
58
+ });
59
+
60
+ it('cleans up portal content when destroyed via conditional rendering', () => {
61
+ component TestPortal() {
62
+ let open = track(true);
63
+
64
+ if (@open) {
65
+ <Portal target={document.body}><div class='test-portal'>{'Conditional content'}</div></Portal>
66
+ }
67
+
68
+ <button onClick={() => @open = false}>{'Close'}</button>
69
+ }
70
+
71
+ render(TestPortal);
72
+
73
+ // Initially portal content should be present
74
+ expect(document.body.querySelector('.test-portal')).toBeTruthy();
75
+
76
+ // Click close button to destroy portal
77
+ container.querySelector('button').click();
78
+ flushSync();
79
+
80
+ // Portal content should be cleaned up
81
+ expect(document.body.querySelector('.test-portal')).toBeNull();
82
+ });
83
+
84
+ it('opens and closes portal via conditional rendering', () => {
85
+ component TestPortal() {
86
+ let open = track(false);
87
+
88
+ if (@open) {
89
+ <Portal target={document.body}><div class='test-portal'>
90
+ {'Content'}
91
+ <button onClick={() => @open = false}>{'Close'}</button>
92
+ </div></Portal>
93
+ }
94
+
95
+ if (!@open) {
96
+ <button onClick={() => @open = true}>{'Open'}</button>
97
+ }
98
+ }
99
+
100
+ render(TestPortal);
101
+
102
+ // Open the portal
103
+ container.querySelector('button').click();
104
+ flushSync();
105
+ expect(document.body.querySelector('.test-portal')).toBeTruthy();
106
+
107
+ // Close the portal - this should work without errors
108
+ expect(() => {
109
+ document.body.querySelector('button').click();
110
+ flushSync();
111
+ }).not.toThrow();
112
+
113
+ // Portal content should be cleaned up
114
+ expect(document.body.querySelector('.test-portal')).toBeNull();
115
+ });
116
+
117
+ it('handles multiple portals simultaneously', () => {
118
+ const target1 = document.createElement('div');
119
+ const target2 = document.createElement('div');
120
+ target1.id = 'multi-target1';
121
+ target2.id = 'multi-target2';
122
+ document.body.appendChild(target1);
123
+ document.body.appendChild(target2);
124
+
125
+ component TestMultiPortal() {
126
+ <Portal target={target1}><div class='test-portal'>{'Portal 1 content'}</div></Portal>
127
+
128
+ <Portal target={target2}><div class='test-portal'>{'Portal 2 content'}</div></Portal>
129
+ }
130
+
131
+ render(TestMultiPortal);
132
+
133
+ // Both portals should render in their respective targets
134
+ expect(target1.querySelector('.test-portal')).toBeTruthy();
135
+ expect(target1.querySelector('.test-portal').textContent).toBe('Portal 1 content');
136
+
137
+ expect(target2.querySelector('.test-portal')).toBeTruthy();
138
+ expect(target2.querySelector('.test-portal').textContent).toBe('Portal 2 content');
139
+
140
+ document.body.removeChild(target1);
141
+ document.body.removeChild(target2);
142
+ });
143
+
144
+ it('handles portal with reactive content', () => {
145
+ component TestReactivePortal() {
146
+ let count = track(0);
147
+
148
+ <Portal target={document.body}><div class='test-portal'>
149
+ {'Count: '}
150
+ {String(@count)}
151
+ <button onClick={() => @count++}>{'Increment'}</button>
152
+ </div></Portal>
153
+ }
154
+
155
+ render(TestReactivePortal);
156
+
157
+ const portalElement = document.body.querySelector('.test-portal');
158
+ expect(portalElement).toBeTruthy();
159
+ expect(portalElement.textContent).toContain('Count: 0');
160
+
161
+ // Click increment button
162
+ portalElement.querySelector('button').click();
163
+ flushSync();
164
+
165
+ expect(portalElement.textContent).toContain('Count: 1');
166
+ });
167
+ });