@ripple-ts/compat-react 0.2.166 → 0.2.168

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ripple-ts/compat-react",
3
- "version": "0.2.166",
3
+ "version": "0.2.168",
4
4
  "description": "Ripple compatibility layer for React",
5
5
  "main": "src/index.js",
6
6
  "author": "Dominic Gannaway",
@@ -17,7 +17,7 @@
17
17
  "dependencies": {
18
18
  "react": "^19.2.0",
19
19
  "react-dom": "^19.2.0",
20
- "ripple": "0.2.166"
20
+ "ripple": "0.2.168"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@types/react": "^19.2.2",
package/src/index.js CHANGED
@@ -5,6 +5,7 @@ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
5
  import {
6
6
  useSyncExternalStore,
7
7
  useLayoutEffect,
8
+ useEffect,
8
9
  useRef,
9
10
  useState,
10
11
  Component,
@@ -24,6 +25,8 @@ import {
24
25
  suspend,
25
26
  TRY_BLOCK,
26
27
  destroy_block,
28
+ root,
29
+ init_operations,
27
30
  } from 'ripple/internal/client';
28
31
  import { Context } from 'ripple';
29
32
 
@@ -202,7 +205,7 @@ export function Ripple({ component, props }) {
202
205
  }
203
206
  const portals = portals_ref.current;
204
207
 
205
- useLayoutEffect(() => {
208
+ useEffect(() => {
206
209
  const span = /** @type {HTMLSpanElement | null} */ (ref.current);
207
210
  if (span === null) {
208
211
  return;
@@ -210,6 +213,12 @@ export function Ripple({ component, props }) {
210
213
  const frag = document.createDocumentFragment();
211
214
  const anchor = document.createTextNode('');
212
215
  const block = get_block_from_dom(span);
216
+
217
+ if (block === null) {
218
+ throw new Error(
219
+ 'Ripple component must be rendered inside a Ripple root. If you are using Ripple inside a React app, ensure your React root contains <RippleRoot>.',
220
+ );
221
+ }
213
222
  const tracked_props = (tracked_props_ref.current = tracked(props || {}, block));
214
223
  const proxied_props = proxy_props(() => get_tracked(tracked_props));
215
224
  frag.append(anchor);
@@ -230,7 +239,7 @@ export function Ripple({ component, props }) {
230
239
  };
231
240
  }, [component]);
232
241
 
233
- useLayoutEffect(() => {
242
+ useEffect(() => {
234
243
  set(/** @type {any} */ (tracked_props_ref.current), props || {});
235
244
  }, [props]);
236
245
 
@@ -241,3 +250,23 @@ export function Ripple({ component, props }) {
241
250
  ],
242
251
  });
243
252
  }
253
+
254
+ /**
255
+ * @param {{ children: React.ReactNode }} props
256
+ */
257
+ export function RippleRoot({ children }) {
258
+ const ref = useRef(null);
259
+
260
+ useLayoutEffect(() => {
261
+ const target_element = /** @type {HTMLSpanElement | null} */ (ref.current);
262
+ if (target_element === null) {
263
+ return;
264
+ }
265
+ init_operations();
266
+ const e = root(() => {});
267
+ // @ts-ignore
268
+ target_element.__ripple_block = e;
269
+ }, []);
270
+
271
+ return jsx('span', { ref, style: { display: 'contents' }, children });
272
+ }
@@ -1,304 +1,421 @@
1
1
  import { track, flushSync } from 'ripple';
2
- import { act } from 'react';
2
+ import { act, createContext, useContext } from 'react';
3
+ import { Ripple } from '@ripple-ts/compat-react';
4
+ import { createRoot } from 'react-dom/client';
5
+ import { jsx, jsxs } from 'react/jsx-runtime';
6
+ import { RippleRoot } from '@ripple-ts/compat-react';
3
7
 
4
8
  describe('compat-react', () => {
5
- it('should render basic React JSX inside tsx:react tags', async () => {
6
- component App() {
7
- <div>
8
- <h1>{'Hello from Ripple'}</h1>
9
- <tsx:react>
10
- <div className="react-content">
11
- {'Hello from React'}
12
- </div>
13
- </tsx:react>
14
- </div>
15
- }
16
-
17
- await act(async () => {
18
- render(App);
9
+ describe('tsx:react integration', () => {
10
+ it('should render basic React JSX inside tsx:react tags', async () => {
11
+ component App() {
12
+ <div>
13
+ <h1>{'Hello from Ripple'}</h1>
14
+ <tsx:react>
15
+ <div className="react-content">Hello from React</div>
16
+ </tsx:react>
17
+ </div>
18
+ }
19
+
20
+ await act(async () => {
21
+ render(App);
22
+ });
23
+
24
+ const rippleHeading = container.querySelector('h1');
25
+ const reactDiv = container.querySelector('.react-content');
26
+ expect(rippleHeading).toBeTruthy();
27
+ expect(rippleHeading.textContent).toBe('Hello from Ripple');
28
+ expect(reactDiv).toBeTruthy();
29
+ expect(reactDiv.textContent).toBe('Hello from React');
19
30
  });
20
31
 
21
- const rippleHeading = container.querySelector('h1');
22
- const reactDiv = container.querySelector('.react-content');
23
- expect(rippleHeading).toBeTruthy();
24
- expect(rippleHeading.textContent).toBe('Hello from Ripple');
25
- expect(reactDiv).toBeTruthy();
26
- expect(reactDiv.textContent).toBe('Hello from React');
27
- });
28
-
29
- it('should render React fragments inside tsx:react tags', async () => {
30
- component App() {
31
- <div>
32
- <tsx:react>
33
- <>
34
- <span className="first">
35
- {'First'}
36
- </span>
37
- <span className="second">
38
- {'Second'}
39
- </span>
40
- </>
41
- </tsx:react>
42
- </div>
43
- }
44
-
45
- await act(async () => {
46
- render(App);
32
+ it('should render React fragments inside tsx:react tags', async () => {
33
+ component App() {
34
+ <div>
35
+ <tsx:react>
36
+ <>
37
+ <span className="first">First</span>
38
+ <span className="second">Second</span>
39
+ </>
40
+ </tsx:react>
41
+ </div>
42
+ }
43
+
44
+ await act(async () => {
45
+ render(App);
46
+ });
47
+
48
+ const first = container.querySelector('.first');
49
+ const second = container.querySelector('.second');
50
+ expect(first).toBeTruthy();
51
+ expect(first.textContent).toBe('First');
52
+ expect(second).toBeTruthy();
53
+ expect(second.textContent).toBe('Second');
47
54
  });
48
55
 
49
- const first = container.querySelector('.first');
50
- const second = container.querySelector('.second');
51
- expect(first).toBeTruthy();
52
- expect(first.textContent).toBe('First');
53
- expect(second).toBeTruthy();
54
- expect(second.textContent).toBe('Second');
55
- });
56
-
57
- it('should render nested React components', async () => {
58
- component App() {
59
- <div>
60
- <tsx:react>
61
- <div className="wrapper">
62
- <div className="inner">
63
- <span className="content">
64
- {'Nested content'}
65
- </span>
56
+ it('should render nested React components', async () => {
57
+ component App() {
58
+ <div>
59
+ <tsx:react>
60
+ <div className="wrapper">
61
+ <div className="inner">
62
+ <span className="content">Nested content</span>
63
+ </div>
66
64
  </div>
67
- </div>
68
- </tsx:react>
69
- </div>
70
- }
71
-
72
- await act(async () => {
73
- render(App);
65
+ </tsx:react>
66
+ </div>
67
+ }
68
+
69
+ await act(async () => {
70
+ render(App);
71
+ });
72
+
73
+ const wrapper = container.querySelector('.wrapper');
74
+ const inner = container.querySelector('.inner');
75
+ const content = container.querySelector('.content');
76
+ expect(wrapper).toBeTruthy();
77
+ expect(inner).toBeTruthy();
78
+ expect(content).toBeTruthy();
79
+ expect(content.textContent).toBe('Nested content');
74
80
  });
75
81
 
76
- const wrapper = container.querySelector('.wrapper');
77
- const inner = container.querySelector('.inner');
78
- const content = container.querySelector('.content');
79
- expect(wrapper).toBeTruthy();
80
- expect(inner).toBeTruthy();
81
- expect(content).toBeTruthy();
82
- expect(content.textContent).toBe('Nested content');
83
- });
84
-
85
- it('should mix Ripple and React content', async () => {
86
- component App() {
87
- <div class="container">
88
- <div class="ripple">{'This is Ripple'}</div>
89
- <tsx:react>
90
- <div className="react">
91
- {'This is React'}
92
- </div>
93
- </tsx:react>
94
- <div class="ripple-2">{'Back to Ripple'}</div>
95
- </div>
96
- }
97
-
98
- await act(async () => {
99
- render(App);
82
+ it('should mix Ripple and React content', async () => {
83
+ component App() {
84
+ <div class="container">
85
+ <div class="ripple">{'This is Ripple'}</div>
86
+ <tsx:react>
87
+ <div className="react">This is React</div>
88
+ </tsx:react>
89
+ <div class="ripple-2">{'Back to Ripple'}</div>
90
+ </div>
91
+ }
92
+
93
+ await act(async () => {
94
+ render(App);
95
+ });
96
+
97
+ const rippleDiv = container.querySelector('.ripple');
98
+ const reactDiv = container.querySelector('.react');
99
+ const rippleDiv2 = container.querySelector('.ripple-2');
100
+ expect(rippleDiv).toBeTruthy();
101
+ expect(rippleDiv.textContent).toBe('This is Ripple');
102
+ expect(reactDiv).toBeTruthy();
103
+ expect(reactDiv.textContent).toBe('This is React');
104
+ expect(rippleDiv2).toBeTruthy();
105
+ expect(rippleDiv2.textContent).toBe('Back to Ripple');
100
106
  });
101
107
 
102
- const rippleDiv = container.querySelector('.ripple');
103
- const reactDiv = container.querySelector('.react');
104
- const rippleDiv2 = container.querySelector('.ripple-2');
105
- expect(rippleDiv).toBeTruthy();
106
- expect(rippleDiv.textContent).toBe('This is Ripple');
107
- expect(reactDiv).toBeTruthy();
108
- expect(reactDiv.textContent).toBe('This is React');
109
- expect(rippleDiv2).toBeTruthy();
110
- expect(rippleDiv2.textContent).toBe('Back to Ripple');
111
- });
112
-
113
- it('should handle multiple tsx:react blocks', async () => {
114
- component App() {
115
- <div>
116
- <tsx:react>
117
- <div className="react-1">
118
- {'React Block 1'}
119
- </div>
120
- </tsx:react>
121
- <div class="ripple-middle">{'Ripple in between'}</div>
122
- <tsx:react>
123
- <div className="react-2">
124
- {'React Block 2'}
125
- </div>
126
- </tsx:react>
127
- </div>
128
- }
129
-
130
- await act(async () => {
131
- render(App);
108
+ it('should handle multiple tsx:react blocks', async () => {
109
+ component App() {
110
+ <div>
111
+ <tsx:react>
112
+ <div className="react-1">React Block 1</div>
113
+ </tsx:react>
114
+ <div class="ripple-middle">{'Ripple in between'}</div>
115
+ <tsx:react>
116
+ <div className="react-2">React Block 2</div>
117
+ </tsx:react>
118
+ </div>
119
+ }
120
+
121
+ await act(async () => {
122
+ render(App);
123
+ });
124
+
125
+ const react1 = container.querySelector('.react-1');
126
+ const middle = container.querySelector('.ripple-middle');
127
+ const react2 = container.querySelector('.react-2');
128
+ expect(react1).toBeTruthy();
129
+ expect(react1.textContent).toBe('React Block 1');
130
+ expect(middle).toBeTruthy();
131
+ expect(middle.textContent).toBe('Ripple in between');
132
+ expect(react2).toBeTruthy();
133
+ expect(react2.textContent).toBe('React Block 2');
132
134
  });
133
135
 
134
- const react1 = container.querySelector('.react-1');
135
- const middle = container.querySelector('.ripple-middle');
136
- const react2 = container.querySelector('.react-2');
137
- expect(react1).toBeTruthy();
138
- expect(react1.textContent).toBe('React Block 1');
139
- expect(middle).toBeTruthy();
140
- expect(middle.textContent).toBe('Ripple in between');
141
- expect(react2).toBeTruthy();
142
- expect(react2.textContent).toBe('React Block 2');
143
- });
136
+ it('should handle React components with attributes', async () => {
137
+ component App() {
138
+ <div>
139
+ <tsx:react>
140
+ <div className="react" id="test-id">
141
+ <span>Content</span>
142
+ </div>
143
+ </tsx:react>
144
+ </div>
145
+ }
146
+
147
+ await act(async () => {
148
+ render(App);
149
+ });
150
+
151
+ const reactDiv = container.querySelector('.react');
152
+ expect(reactDiv).toBeTruthy();
153
+ expect(reactDiv.id).toBe('test-id');
154
+ expect(reactDiv.querySelector('span').textContent).toBe('Content');
155
+ });
144
156
 
145
- it('should handle React components with attributes', async () => {
146
- component App() {
147
- <div>
148
- <tsx:react>
149
- <div className="react" id="test-id">
150
- <span>
151
- {'Content'}
152
- </span>
153
- </div>
154
- </tsx:react>
155
- </div>
156
- }
157
+ it('should handle nested fragments', async () => {
158
+ component App() {
159
+ <div>
160
+ <tsx:react>
161
+ <>
162
+ <div className="outer">Outer</div>
163
+ <>
164
+ <div className="inner">Inner</div>
165
+ </>
166
+ </>
167
+ </tsx:react>
168
+ </div>
169
+ }
170
+
171
+ await act(async () => {
172
+ render(App);
173
+ });
174
+
175
+ const outer = container.querySelector('.outer');
176
+ const inner = container.querySelector('.inner');
177
+ expect(outer).toBeTruthy();
178
+ expect(outer.textContent).toBe('Outer');
179
+ expect(inner).toBeTruthy();
180
+ expect(inner.textContent).toBe('Inner');
181
+ });
157
182
 
158
- await act(async () => {
159
- render(App);
183
+ it('should handle complex nested structures', async () => {
184
+ component App() {
185
+ <div>
186
+ <tsx:react>
187
+ <div className="list">
188
+ <ul>
189
+ <li>Item 1</li>
190
+ <li>Item 2</li>
191
+ <li>Item 3</li>
192
+ </ul>
193
+ </div>
194
+ </tsx:react>
195
+ </div>
196
+ }
197
+
198
+ await act(async () => {
199
+ render(App);
200
+ });
201
+
202
+ const list = container.querySelector('.list');
203
+ const items = container.querySelectorAll('li');
204
+ expect(list).toBeTruthy();
205
+ expect(items.length).toBe(3);
206
+ expect(items[0].textContent).toBe('Item 1');
207
+ expect(items[1].textContent).toBe('Item 2');
208
+ expect(items[2].textContent).toBe('Item 3');
160
209
  });
161
210
 
162
- const reactDiv = container.querySelector('.react');
163
- expect(reactDiv).toBeTruthy();
164
- expect(reactDiv.id).toBe('test-id');
165
- expect(reactDiv.querySelector('span').textContent).toBe('Content');
166
- });
211
+ it('should handle empty fragments', async () => {
212
+ component App() {
213
+ <div>
214
+ <tsx:react>
215
+ <></>
216
+ </tsx:react>
217
+ <div class="after">{'After empty fragment'}</div>
218
+ </div>
219
+ }
220
+
221
+ await act(async () => {
222
+ render(App);
223
+ });
224
+
225
+ const after = container.querySelector('.after');
226
+ expect(after).toBeTruthy();
227
+ expect(after.textContent).toBe('After empty fragment');
228
+ });
167
229
 
168
- it('should handle nested fragments', async () => {
169
- component App() {
170
- <div>
171
- <tsx:react>
172
- <>
173
- <div className="outer">
174
- {'Outer'}
230
+ it('should work with Ripple reactivity', async () => {
231
+ component App() {
232
+ let count = track(0);
233
+ <div>
234
+ <div class="ripple-count">{@count}</div>
235
+ <button onClick={() => @count++}>{'Increment'}</button>
236
+ <tsx:react>
237
+ <div className="react-message">
238
+ {'React content is static'}
175
239
  </div>
176
- <>
177
- <div className="inner">
178
- {'Inner'}
179
- </div>
180
- </>
181
- </>
182
- </tsx:react>
183
- </div>
184
- }
240
+ </tsx:react>
241
+ </div>
242
+ }
243
+
244
+ await act(async () => {
245
+ render(App);
246
+ });
247
+
248
+ const rippleCount = container.querySelector('.ripple-count');
249
+ const button = container.querySelector('button');
250
+ const reactMessage = container.querySelector('.react-message');
251
+ expect(rippleCount.textContent).toBe('0');
252
+ expect(reactMessage.textContent).toBe('React content is static');
253
+ button.click();
254
+ flushSync();
255
+ expect(rippleCount.textContent).toBe('1');
256
+ expect(reactMessage.textContent).toBe('React content is static');
257
+ button.click();
258
+ flushSync();
259
+ expect(rippleCount.textContent).toBe('2');
260
+ });
185
261
 
186
- await act(async () => {
187
- render(App);
262
+ it('should handle a call expression at the top-level of a tsx:react block', async () => {
263
+ function renderReactText() {
264
+ return 'This is rendered from a React component!';
265
+ }
266
+
267
+ component App() {
268
+ <div>
269
+ <tsx:react>
270
+ {renderReactText()}
271
+ </tsx:react>
272
+ </div>
273
+ }
274
+
275
+ await act(async () => {
276
+ render(App);
277
+ });
278
+
279
+ const reactContent = container.querySelector('div > div');
280
+ expect(reactContent).toBeTruthy();
281
+ expect(reactContent.textContent).toBe('This is rendered from a React component!');
188
282
  });
189
283
 
190
- const outer = container.querySelector('.outer');
191
- const inner = container.querySelector('.inner');
192
- expect(outer).toBeTruthy();
193
- expect(outer.textContent).toBe('Outer');
194
- expect(inner).toBeTruthy();
195
- expect(inner.textContent).toBe('Inner');
196
- });
284
+ it('should handle a React context propagation', async () => {
285
+ const SomeContext = createContext('Default Value');
286
+
287
+ function ReactChild() {
288
+ return useContext(SomeContext);
289
+ }
290
+
291
+ component App() {
292
+ <div>
293
+ <tsx:react>
294
+ <SomeContext.Provider value="Provided Value">
295
+ <ReactChild />
296
+ </SomeContext.Provider>
297
+ </tsx:react>
298
+ </div>
299
+ }
300
+
301
+ await act(async () => {
302
+ render(App);
303
+ });
304
+
305
+ const reactContent = container.querySelector('div > div');
306
+ expect(reactContent).toBeTruthy();
307
+ expect(reactContent.textContent).toBe('Provided Value');
308
+ });
197
309
 
198
- it('should handle complex nested structures', async () => {
199
- component App() {
200
- <div>
201
- <tsx:react>
202
- <div className="list">
203
- <ul>
204
- <li>
205
- {'Item 1'}
206
- </li>
207
- <li>
208
- {'Item 2'}
209
- </li>
210
- <li>
211
- {'Item 3'}
212
- </li>
213
- </ul>
310
+ it('should handle a React errors', async () => {
311
+ function ReactChild() {
312
+ throw new Error('Test Error');
313
+ }
314
+
315
+ component App() {
316
+ try {
317
+ <div>
318
+ <tsx:react>
319
+ <ReactChild />
320
+ </tsx:react>
214
321
  </div>
215
- </tsx:react>
216
- </div>
217
- }
218
-
219
- await act(async () => {
220
- render(App);
322
+ } catch (e) {
323
+ <div class="error">{'ReactChiild had an error'}</div>
324
+ }
325
+ }
326
+
327
+ await act(async () => {
328
+ render(App);
329
+ });
330
+
331
+ const reactContent = container.querySelector('div > div');
332
+ expect(reactContent).toBeTruthy();
333
+ expect(reactContent.textContent).toBe('ReactChiild had an error');
221
334
  });
222
335
 
223
- const list = container.querySelector('.list');
224
- const items = container.querySelectorAll('li');
225
- expect(list).toBeTruthy();
226
- expect(items.length).toBe(3);
227
- expect(items[0].textContent).toBe('Item 1');
228
- expect(items[1].textContent).toBe('Item 2');
229
- expect(items[2].textContent).toBe('Item 3');
230
- });
336
+ it('should handle a React errors #2', async () => {
337
+ function ReactChild() {
338
+ throw new Error('Test Error');
339
+ }
231
340
 
232
- it('should handle empty fragments', async () => {
233
- component App() {
234
- <div>
341
+ component RippleChild() {
235
342
  <tsx:react>
236
- <></>
343
+ <ReactChild />
237
344
  </tsx:react>
238
- <div class="after">{'After empty fragment'}</div>
239
- </div>
240
- }
241
-
242
- await act(async () => {
243
- render(App);
345
+ }
346
+
347
+ component App() {
348
+ try {
349
+ <div>
350
+ <tsx:react>
351
+ <Ripple component={RippleChild} />
352
+ </tsx:react>
353
+ </div>
354
+ } catch (e) {
355
+ <div class="error">{'ReactChiild had an error'}</div>
356
+ }
357
+ }
358
+
359
+ await act(async () => {
360
+ render(App);
361
+ });
362
+
363
+ const reactContent = container.querySelector('div > div');
364
+ expect(reactContent).toBeTruthy();
365
+ expect(reactContent.textContent).toBe('ReactChiild had an error');
244
366
  });
245
-
246
- const after = container.querySelector('.after');
247
- expect(after).toBeTruthy();
248
- expect(after.textContent).toBe('After empty fragment');
249
367
  });
250
368
 
251
- it('should work with Ripple reactivity', async () => {
252
- component App() {
253
- let count = track(0);
254
- <div>
255
- <div class="ripple-count">{@count}</div>
256
- <button onClick={() => @count++}>{'Increment'}</button>
257
- <tsx:react>
258
- <div className="react-message">
259
- {'React content is static'}
260
- </div>
261
- </tsx:react>
262
- </div>
263
- }
369
+ describe('Ripple in React app', () => {
370
+ let container: null | Node;
264
371
 
265
- await act(async () => {
266
- render(App);
372
+ beforeEach(() => {
373
+ container = document.createElement('div');
374
+ document.body.appendChild(container);
267
375
  });
268
376
 
269
- const rippleCount = container.querySelector('.ripple-count');
270
- const button = container.querySelector('button');
271
- const reactMessage = container.querySelector('.react-message');
272
- expect(rippleCount.textContent).toBe('0');
273
- expect(reactMessage.textContent).toBe('React content is static');
274
- button.click();
275
- flushSync();
276
- expect(rippleCount.textContent).toBe('1');
277
- expect(reactMessage.textContent).toBe('React content is static');
278
- button.click();
279
- flushSync();
280
- expect(rippleCount.textContent).toBe('2');
281
- });
377
+ afterEach(() => {
378
+ document.body.removeChild(container!);
379
+ container = null;
380
+ });
282
381
 
283
- it('should handle a call expression at the top-level of a tsx:react block', async () => {
284
- function renderReactText() {
285
- return 'This is rendered from a React component!';
286
- }
382
+ it('should render a basic React app', async () => {
383
+ function App() {
384
+ return jsx('div', { children: 'Hello from React App with RippleRoot' });
385
+ }
287
386
 
288
- component App() {
289
- <div>
290
- <tsx:react>
291
- {renderReactText()}
292
- </tsx:react>
293
- </div>
294
- }
387
+ const root = createRoot(container!);
295
388
 
296
- await act(async () => {
297
- render(App);
389
+ await act(async () => {
390
+ root.render(jsx(RippleRoot, { children: jsx(App, {}) }));
391
+ });
392
+
393
+ expect(container!.textContent).toBe('Hello from React App with RippleRoot');
298
394
  });
299
395
 
300
- const reactContent = container.querySelector('div > div');
301
- expect(reactContent).toBeTruthy();
302
- expect(reactContent.textContent).toBe('This is rendered from a React component!');
396
+ it('should render a basic React app with RippleRoot', async () => {
397
+ component RippleComponent() {
398
+ <div>{'Hello from Ripple Component'}</div>
399
+ }
400
+
401
+ function App() {
402
+ return jsxs('div', {
403
+ children: [
404
+ 'Hello from React App with RippleRoot\n',
405
+ jsx(Ripple, { component: RippleComponent }),
406
+ ],
407
+ });
408
+ }
409
+
410
+ const root = createRoot(container!);
411
+
412
+ await act(async () => {
413
+ root.render(jsx(RippleRoot, { children: jsx(App, {}) }));
414
+ });
415
+
416
+ expect(container!.textContent).toBe(
417
+ 'Hello from React App with RippleRoot\nHello from Ripple Component',
418
+ );
419
+ });
303
420
  });
304
421
  });
package/tests/setup.js CHANGED
@@ -1,7 +1,14 @@
1
- import { beforeEach, afterEach } from 'vitest';
1
+ import { beforeEach, afterEach, vi } from 'vitest';
2
2
  import { mount } from 'ripple';
3
3
  import { createReactCompat } from '../src/index.js';
4
4
 
5
+ // Configure React testing environment for act() support
6
+ globalThis.IS_REACT_ACT_ENVIRONMENT = true;
7
+
8
+ // Suppress React error/warning logs during tests
9
+ vi.spyOn(console, 'error').mockImplementation(() => {});
10
+ vi.spyOn(console, 'warn').mockImplementation(() => {});
11
+
5
12
  /**
6
13
  * @param {() => void} component
7
14
  */
package/types/index.d.ts CHANGED
@@ -12,3 +12,5 @@ export declare function createReactCompat(): {
12
12
  };
13
13
 
14
14
  export declare function Ripple<P>(component: Component<P>, props?: P): React.JSX.Element;
15
+
16
+ export declare function RippleRoot(props: { children: React.ReactNode }): React.JSX.Element;