@preact/signals-react 1.2.2 → 1.3.0

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,53 +1,81 @@
1
1
  // @ts-ignore-next-line
2
2
  globalThis.IS_REACT_ACT_ENVIRONMENT = true;
3
3
 
4
- import { signal, useComputed, useSignalEffect } from "@preact/signals-react";
5
- import { createElement, useMemo, memo, StrictMode, createRef } from "react";
6
- import { createRoot, Root } from "react-dom/client";
4
+ import {
5
+ signal,
6
+ computed,
7
+ useComputed,
8
+ useSignalEffect,
9
+ useSignal,
10
+ } from "@preact/signals-react";
11
+ import {
12
+ createElement,
13
+ forwardRef,
14
+ useMemo,
15
+ useReducer,
16
+ memo,
17
+ StrictMode,
18
+ createRef,
19
+ } from "react";
20
+
7
21
  import { renderToStaticMarkup } from "react-dom/server";
8
- import { act } from "react-dom/test-utils";
22
+ import { createRoot, Root, act, checkHangingAct } from "./utils";
9
23
 
10
24
  describe("@preact/signals-react", () => {
11
25
  let scratch: HTMLDivElement;
12
26
  let root: Root;
13
- function render(element: Parameters<Root["render"]>[0]) {
14
- act(() => root.render(element));
27
+
28
+ async function render(element: Parameters<Root["render"]>[0]) {
29
+ await act(() => root.render(element));
15
30
  }
16
31
 
17
- beforeEach(() => {
32
+ beforeEach(async () => {
18
33
  scratch = document.createElement("div");
19
- root = createRoot(scratch);
34
+ document.body.appendChild(scratch);
35
+ root = await createRoot(scratch);
20
36
  });
21
37
 
22
- afterEach(() => {
23
- act(() => root.unmount());
38
+ afterEach(async () => {
39
+ checkHangingAct();
40
+ await act(() => root.unmount());
41
+ scratch.remove();
24
42
  });
25
43
 
26
44
  describe("Text bindings", () => {
27
- it("should render text without signals", () => {
28
- render(<span>test</span>);
45
+ it("should render text without signals", async () => {
46
+ await render(<span>test</span>);
29
47
  const span = scratch.firstChild;
30
48
  const text = span?.firstChild;
31
49
  expect(text).to.have.property("data", "test");
32
50
  });
33
51
 
34
- it("should render Signals as Text", () => {
52
+ it("should render Signals as Text", async () => {
35
53
  const sig = signal("test");
36
- render(<span>{sig}</span>);
54
+ await render(<span>{sig}</span>);
37
55
  const span = scratch.firstChild;
38
56
  expect(span).to.have.property("firstChild").that.is.an.instanceOf(Text);
39
57
  const text = span?.firstChild;
40
58
  expect(text).to.have.property("data", "test");
41
59
  });
42
60
 
43
- it("should update Signal-based Text (no parent component)", () => {
61
+ it("should render computed as Text", async () => {
44
62
  const sig = signal("test");
45
- render(<span>{sig}</span>);
63
+ const comp = computed(() => `${sig} ${sig}`);
64
+ await render(<span>{comp}</span>);
65
+ const span = scratch.firstChild;
66
+ expect(span).to.have.property("firstChild").that.is.an.instanceOf(Text);
67
+ const text = span?.firstChild;
68
+ expect(text).to.have.property("data", "test test");
69
+ });
70
+
71
+ it("should update Signal-based Text (no parent component)", async () => {
72
+ const sig = signal("test");
73
+ await render(<span>{sig}</span>);
46
74
 
47
75
  const text = scratch.firstChild!.firstChild!;
48
76
  expect(text).to.have.property("data", "test");
49
77
 
50
- act(() => {
78
+ await act(() => {
51
79
  sig.value = "changed";
52
80
  });
53
81
 
@@ -57,17 +85,17 @@ describe("@preact/signals-react", () => {
57
85
  expect(text).to.have.property("data", "changed");
58
86
  });
59
87
 
60
- it("should update Signal-based Text (in a parent component)", () => {
88
+ it("should update Signal-based Text (in a parent component)", async () => {
61
89
  const sig = signal("test");
62
90
  function App({ x }: { x: typeof sig }) {
63
91
  return <span>{x}</span>;
64
92
  }
65
- render(<App x={sig} />);
93
+ await render(<App x={sig} />);
66
94
 
67
95
  const text = scratch.firstChild!.firstChild!;
68
96
  expect(text).to.have.property("data", "test");
69
97
 
70
- act(() => {
98
+ await act(() => {
71
99
  sig.value = "changed";
72
100
  });
73
101
 
@@ -79,7 +107,7 @@ describe("@preact/signals-react", () => {
79
107
  });
80
108
 
81
109
  describe("Component bindings", () => {
82
- it("should subscribe to signals", () => {
110
+ it("should subscribe to signals", async () => {
83
111
  const sig = signal("foo");
84
112
 
85
113
  function App() {
@@ -87,16 +115,16 @@ describe("@preact/signals-react", () => {
87
115
  return <p>{value}</p>;
88
116
  }
89
117
 
90
- render(<App />);
118
+ await render(<App />);
91
119
  expect(scratch.textContent).to.equal("foo");
92
120
 
93
- act(() => {
121
+ await act(() => {
94
122
  sig.value = "bar";
95
123
  });
96
124
  expect(scratch.textContent).to.equal("bar");
97
125
  });
98
126
 
99
- it("should activate signal accessed in render", () => {
127
+ it("should activate signal accessed in render", async () => {
100
128
  const sig = signal(null);
101
129
 
102
130
  function App() {
@@ -111,11 +139,14 @@ describe("@preact/signals-react", () => {
111
139
  return <p>{str}</p>;
112
140
  }
113
141
 
114
- const fn = () => render(<App />);
115
- expect(fn).not.to.throw;
142
+ try {
143
+ await render(<App />);
144
+ } catch (e: any) {
145
+ expect.fail(e.stack);
146
+ }
116
147
  });
117
148
 
118
- it("should not subscribe to child signals", () => {
149
+ it("should not subscribe to child signals", async () => {
119
150
  const sig = signal("foo");
120
151
 
121
152
  function Child() {
@@ -129,10 +160,10 @@ describe("@preact/signals-react", () => {
129
160
  return <Child />;
130
161
  }
131
162
 
132
- render(<App />);
163
+ await render(<App />);
133
164
  expect(scratch.textContent).to.equal("foo");
134
165
 
135
- act(() => {
166
+ await act(() => {
136
167
  sig.value = "bar";
137
168
  });
138
169
  expect(spy).to.be.calledOnce;
@@ -148,20 +179,40 @@ describe("@preact/signals-react", () => {
148
179
 
149
180
  function App() {
150
181
  sig.value;
151
- return useMemo(() => <Inner foo={1} />, []);
182
+ return useMemo(() => <Inner />, []);
152
183
  }
153
184
 
154
- render(<App />);
185
+ await render(<App />);
155
186
  expect(scratch.textContent).to.equal("foo");
156
187
 
157
- act(() => {
188
+ await act(() => {
189
+ sig.value = "bar";
190
+ });
191
+ expect(scratch.textContent).to.equal("bar");
192
+ });
193
+
194
+ it("should update forwardRef'ed component via signals", async () => {
195
+ const sig = signal("foo");
196
+
197
+ const Inner = forwardRef(() => {
198
+ return <p>{sig.value}</p>;
199
+ });
200
+
201
+ function App() {
202
+ return <Inner />;
203
+ }
204
+
205
+ await render(<App />);
206
+ expect(scratch.textContent).to.equal("foo");
207
+
208
+ await act(() => {
158
209
  sig.value = "bar";
159
210
  });
160
211
  expect(scratch.textContent).to.equal("bar");
161
212
  });
162
213
 
163
214
  it("should consistently rerender in strict mode", async () => {
164
- const sig = signal<string>(null!);
215
+ const sig = signal(-1);
165
216
 
166
217
  const Test = () => <p>{sig.value}</p>;
167
218
  const App = () => (
@@ -170,18 +221,19 @@ describe("@preact/signals-react", () => {
170
221
  </StrictMode>
171
222
  );
172
223
 
173
- for (let i = 0; i < 3; i++) {
174
- const value = `${i}`;
224
+ await render(<App />);
225
+ expect(scratch.textContent).to.equal("-1");
175
226
 
176
- act(() => {
177
- sig.value = value;
178
- render(<App />);
227
+ for (let i = 0; i < 3; i++) {
228
+ await act(async () => {
229
+ sig.value = i;
179
230
  });
180
- expect(scratch.textContent).to.equal(value);
231
+ expect(scratch.textContent).to.equal("" + i);
181
232
  }
182
233
  });
234
+
183
235
  it("should consistently rerender in strict mode (with memo)", async () => {
184
- const sig = signal<string>(null!);
236
+ const sig = signal(-1);
185
237
 
186
238
  const Test = memo(() => <p>{sig.value}</p>);
187
239
  const App = () => (
@@ -190,16 +242,17 @@ describe("@preact/signals-react", () => {
190
242
  </StrictMode>
191
243
  );
192
244
 
193
- for (let i = 0; i < 3; i++) {
194
- const value = `${i}`;
245
+ await render(<App />);
246
+ expect(scratch.textContent).to.equal("-1");
195
247
 
196
- act(() => {
197
- sig.value = value;
198
- render(<App />);
248
+ for (let i = 0; i < 3; i++) {
249
+ await act(async () => {
250
+ sig.value = i;
199
251
  });
200
- expect(scratch.textContent).to.equal(value);
252
+ expect(scratch.textContent).to.equal("" + i);
201
253
  }
202
254
  });
255
+
203
256
  it("should render static markup of a component", async () => {
204
257
  const count = signal(0);
205
258
 
@@ -211,16 +264,87 @@ describe("@preact/signals-react", () => {
211
264
  </pre>
212
265
  );
213
266
  };
267
+
268
+ await render(<Test />);
269
+ expect(scratch.textContent).to.equal("<code>0</code><code>0</code>");
270
+
214
271
  for (let i = 0; i < 3; i++) {
215
- act(() => {
272
+ await act(async () => {
216
273
  count.value += 1;
217
- render(<Test />);
218
274
  });
219
275
  expect(scratch.textContent).to.equal(
220
276
  `<code>${count.value}</code><code>${count.value}</code>`
221
277
  );
222
278
  }
223
279
  });
280
+
281
+ it("should correctly render components that have useReducer()", async () => {
282
+ const count = signal(0);
283
+
284
+ let increment: () => void;
285
+ const Test = () => {
286
+ const [state, dispatch] = useReducer(
287
+ (state: number, action: number) => {
288
+ return state + action;
289
+ },
290
+ -2
291
+ );
292
+
293
+ increment = () => dispatch(1);
294
+
295
+ const doubled = count.value * 2;
296
+
297
+ return (
298
+ <pre>
299
+ <code>{state}</code>
300
+ <code>{doubled}</code>
301
+ </pre>
302
+ );
303
+ };
304
+
305
+ await render(<Test />);
306
+ expect(scratch.innerHTML).to.equal(
307
+ "<pre><code>-2</code><code>0</code></pre>"
308
+ );
309
+
310
+ for (let i = 0; i < 3; i++) {
311
+ await act(async () => {
312
+ count.value += 1;
313
+ });
314
+ expect(scratch.innerHTML).to.equal(
315
+ `<pre><code>-2</code><code>${count.value * 2}</code></pre>`
316
+ );
317
+ }
318
+
319
+ await act(() => {
320
+ increment();
321
+ });
322
+ expect(scratch.innerHTML).to.equal(
323
+ `<pre><code>-1</code><code>${count.value * 2}</code></pre>`
324
+ );
325
+ });
326
+ });
327
+
328
+ describe("useSignal()", () => {
329
+ it("should create a signal from a primitive value", async () => {
330
+ function App() {
331
+ const count = useSignal(1);
332
+ return (
333
+ <div>
334
+ {count}
335
+ <button onClick={() => count.value++}>Increment</button>
336
+ </div>
337
+ );
338
+ }
339
+
340
+ await render(<App />);
341
+ expect(scratch.textContent).to.equal("1Increment");
342
+
343
+ await act(() => {
344
+ scratch.querySelector("button")!.click();
345
+ });
346
+ expect(scratch.textContent).to.equal("2Increment");
347
+ });
224
348
  });
225
349
 
226
350
  describe("useSignalEffect()", () => {
@@ -245,7 +369,7 @@ describe("@preact/signals-react", () => {
245
369
  );
246
370
  }
247
371
 
248
- render(<App />);
372
+ await render(<App />);
249
373
  expect(scratch.textContent).to.equal("foo");
250
374
 
251
375
  expect(spy).to.have.been.calledOnceWith(
@@ -256,7 +380,7 @@ describe("@preact/signals-react", () => {
256
380
 
257
381
  spy.resetHistory();
258
382
 
259
- act(() => {
383
+ await act(() => {
260
384
  sig.value = "bar";
261
385
  });
262
386
 
@@ -294,7 +418,7 @@ describe("@preact/signals-react", () => {
294
418
  );
295
419
  }
296
420
 
297
- render(<App />);
421
+ await render(<App />);
298
422
 
299
423
  expect(cleanup).not.to.have.been.called;
300
424
  expect(spy).to.have.been.calledOnceWith(
@@ -304,7 +428,7 @@ describe("@preact/signals-react", () => {
304
428
  );
305
429
  spy.resetHistory();
306
430
 
307
- act(() => {
431
+ await act(() => {
308
432
  sig.value = "bar";
309
433
  });
310
434
 
@@ -336,16 +460,18 @@ describe("@preact/signals-react", () => {
336
460
  return <p ref={ref}>{sig.value}</p>;
337
461
  }
338
462
 
339
- render(<App />);
463
+ await render(<App />);
340
464
 
341
465
  const child = scratch.firstElementChild;
342
466
 
467
+ expect(scratch.innerHTML).to.equal("<p>foo</p>");
343
468
  expect(cleanup).not.to.have.been.called;
344
469
  expect(spy).to.have.been.calledOnceWith("foo", child);
345
470
  spy.resetHistory();
346
471
 
347
- render(null);
472
+ await render(null);
348
473
 
474
+ expect(scratch.innerHTML).to.equal("");
349
475
  expect(spy).not.to.have.been.called;
350
476
  expect(cleanup).to.have.been.calledOnce;
351
477
  // @note: React cleans up the ref eagerly, so it's already null by the time the callback runs.
@@ -0,0 +1,49 @@
1
+ // @ts-ignore-next-line
2
+ globalThis.IS_REACT_ACT_ENVIRONMENT = true;
3
+
4
+ import { signal } from "@preact/signals-react";
5
+ import { createElement } from "react";
6
+ import { Route, Routes, MemoryRouter } from "react-router-dom";
7
+
8
+ import { act, checkHangingAct, createRoot, Root } from "./utils";
9
+
10
+ describe("@preact/signals-react", () => {
11
+ let scratch: HTMLDivElement;
12
+ let root: Root;
13
+ async function render(element: Parameters<Root["render"]>[0]) {
14
+ await act(() => root.render(element));
15
+ }
16
+
17
+ beforeEach(async () => {
18
+ scratch = document.createElement("div");
19
+ document.body.appendChild(scratch);
20
+ root = await createRoot(scratch);
21
+ });
22
+
23
+ afterEach(async () => {
24
+ checkHangingAct();
25
+ await act(() => root.unmount());
26
+ scratch.remove();
27
+ });
28
+
29
+ describe("react-router-dom", () => {
30
+ it("Route component should render", async () => {
31
+ const name = signal("World")!;
32
+
33
+ function App() {
34
+ return (
35
+ <MemoryRouter>
36
+ <Routes>
37
+ <Route path="/page1" element={<div>Page 1</div>}></Route>
38
+ <Route path="*" element={<div>Hello {name}!</div>}></Route>
39
+ </Routes>
40
+ </MemoryRouter>
41
+ );
42
+ }
43
+
44
+ await render(<App />);
45
+
46
+ expect(scratch.innerHTML).to.equal("<div>Hello World!</div>");
47
+ });
48
+ });
49
+ });
package/test/utils.ts ADDED
@@ -0,0 +1,67 @@
1
+ import { act as realAct } from "react-dom/test-utils";
2
+
3
+ export interface Root {
4
+ render(element: JSX.Element | null): void;
5
+ unmount(): void;
6
+ }
7
+
8
+ // We need to use createRoot() if it's available, but it's only available in
9
+ // React 18. To enable local testing with React 16 & 17, we'll create a fake
10
+ // createRoot() that uses render() and unmountComponentAtNode() instead.
11
+ let createRootCache: ((container: Element) => Root) | undefined;
12
+ export async function createRoot(container: Element): Promise<Root> {
13
+ if (!createRootCache) {
14
+ try {
15
+ // @ts-expect-error ESBuild will replace this import with a require() call
16
+ // if it resolves react-dom/client. If it doesn't, it will leave the
17
+ // import untouched causing a runtime error we'll handle below.
18
+ const { createRoot } = await import("react-dom/client");
19
+ createRootCache = createRoot;
20
+ } catch (e) {
21
+ // @ts-expect-error ESBuild will replace this import with a require() call
22
+ // if it resolves react-dom.
23
+ const { render, unmountComponentAtNode } = await import("react-dom");
24
+ createRootCache = (container: Element) => ({
25
+ render(element: JSX.Element) {
26
+ render(element, container);
27
+ },
28
+ unmount() {
29
+ unmountComponentAtNode(container);
30
+ },
31
+ });
32
+ }
33
+ }
34
+
35
+ return createRootCache(container);
36
+ }
37
+
38
+ // When testing using react's production build, we can't use act (React
39
+ // explicitly throws an error in this situation). So instead we'll fake act by
40
+ // just waiting 10ms for React's concurrent rerendering to flush. We'll make a
41
+ // best effort to throw a helpful error in afterEach if we detect that act() was
42
+ // called but not awaited.
43
+ const delay = (ms: number) => new Promise(r => setTimeout(r, ms));
44
+
45
+ let acting = 0;
46
+ async function prodActShim(cb: () => void | Promise<void>): Promise<void> {
47
+ acting++;
48
+ try {
49
+ await cb();
50
+ await delay(10);
51
+ } finally {
52
+ acting--;
53
+ }
54
+ }
55
+
56
+ export function checkHangingAct() {
57
+ if (acting > 0) {
58
+ throw new Error(
59
+ `It appears act() was called but not awaited. This could happen if a test threw an Error or if a test forgot to await a call to act. Make sure to await act() calls in tests.`
60
+ );
61
+ }
62
+ }
63
+
64
+ export const act =
65
+ process.env.NODE_ENV === "production"
66
+ ? (prodActShim as typeof realAct)
67
+ : realAct;