@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 +2 -2
- package/src/index.js +31 -2
- package/tests/index.test.ripple +378 -261
- package/tests/setup.js +8 -1
- package/types/index.d.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ripple-ts/compat-react",
|
|
3
|
-
"version": "0.2.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/tests/index.test.ripple
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
<
|
|
9
|
-
|
|
10
|
-
<
|
|
11
|
-
|
|
12
|
-
</
|
|
13
|
-
</
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
</
|
|
68
|
-
</
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
159
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
<
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
187
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
<
|
|
208
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
233
|
-
component App() {
|
|
234
|
-
<div>
|
|
341
|
+
component RippleChild() {
|
|
235
342
|
<tsx:react>
|
|
236
|
-
|
|
343
|
+
<ReactChild />
|
|
237
344
|
</tsx:react>
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
252
|
-
|
|
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
|
-
|
|
266
|
-
|
|
372
|
+
beforeEach(() => {
|
|
373
|
+
container = document.createElement('div');
|
|
374
|
+
document.body.appendChild(container);
|
|
267
375
|
});
|
|
268
376
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
289
|
-
<div>
|
|
290
|
-
<tsx:react>
|
|
291
|
-
{renderReactText()}
|
|
292
|
-
</tsx:react>
|
|
293
|
-
</div>
|
|
294
|
-
}
|
|
387
|
+
const root = createRoot(container!);
|
|
295
388
|
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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