@preact/signals-react 1.3.0 → 1.3.2

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.
@@ -0,0 +1,24 @@
1
+ import { signal, useSignalEffect } from "@preact/signals-react";
2
+ import { expect } from "chai";
3
+ import { createElement } from "react";
4
+ import { renderToStaticMarkup } from "react-dom/server";
5
+ import sinon from "sinon";
6
+ import { mountSignalsTests } from "../shared/mounting";
7
+
8
+ describe("renderToStaticMarkup", () => {
9
+ mountSignalsTests(renderToStaticMarkup);
10
+
11
+ it("should not invoke useSignalEffect", async () => {
12
+ const spy = sinon.spy();
13
+ const sig = signal("foo");
14
+
15
+ function App() {
16
+ useSignalEffect(() => spy(sig.value));
17
+ return <p>{sig.value}</p>;
18
+ }
19
+
20
+ const html = await renderToStaticMarkup(<App />);
21
+ expect(html).to.equal("<p>foo</p>");
22
+ expect(spy.called).to.be.false;
23
+ });
24
+ });
@@ -0,0 +1,52 @@
1
+ // import register from "@babel/register";
2
+ const register = require("@babel/register").default;
3
+
4
+ const coverage = String(process.env.COVERAGE) === "true";
5
+
6
+ // @babel/register doesn't hook into the experimental NodeJS ESM loader API so
7
+ // we need all test files to run as CommonJS modules in Node
8
+ const env = [
9
+ "@babel/preset-env",
10
+ {
11
+ targets: {
12
+ node: "current",
13
+ },
14
+ loose: true,
15
+ modules: "commonjs",
16
+ },
17
+ ];
18
+
19
+ const jsx = [
20
+ "@babel/preset-react",
21
+ {
22
+ runtime: "classic",
23
+ pragma: "createElement",
24
+ pragmaFrag: "Fragment",
25
+ },
26
+ ];
27
+
28
+ const ts = [
29
+ "@babel/preset-typescript",
30
+ {
31
+ jsxPragma: "createElement",
32
+ jsxPragmaFrag: "Fragment",
33
+ },
34
+ ];
35
+
36
+ register({
37
+ extensions: [".js", ".mjs", ".ts", ".tsx", ".mts", ".mtsx"],
38
+ cache: true,
39
+
40
+ sourceMaps: "inline",
41
+ presets: [ts, jsx, env],
42
+ plugins: [
43
+ coverage && [
44
+ "istanbul",
45
+ {
46
+ // TODO: Currently NodeJS tests always run against dist files. Should we
47
+ // change this?
48
+ // include: minify ? "**/dist/**/*.js" : "**/src/**/*.{js,jsx,ts,tsx}",
49
+ },
50
+ ],
51
+ ].filter(Boolean),
52
+ });
@@ -0,0 +1,184 @@
1
+ // @ts-ignore-next-line
2
+ globalThis.IS_REACT_ACT_ENVIRONMENT = true;
3
+
4
+ import {
5
+ signal,
6
+ computed,
7
+ useComputed,
8
+ useSignal,
9
+ } from "@preact/signals-react";
10
+ import { expect } from "chai";
11
+ import { createElement, useReducer, StrictMode, useState } from "react";
12
+
13
+ import { getConsoleErrorSpy, checkConsoleErrorLogs } from "./utils";
14
+
15
+ export function mountSignalsTests(
16
+ render: (element: JSX.Element) => string | Promise<string>
17
+ ) {
18
+ beforeEach(async () => {
19
+ getConsoleErrorSpy().resetHistory();
20
+ });
21
+
22
+ afterEach(async () => {
23
+ checkConsoleErrorLogs();
24
+ });
25
+
26
+ describe("mount text bindings", () => {
27
+ it("should render text without signals", async () => {
28
+ const html = await render(<span>test</span>);
29
+ expect(html).to.equal("<span>test</span>");
30
+ });
31
+
32
+ it("should render Signals as Text", async () => {
33
+ const sig = signal("test");
34
+ const html = await render(<span>{sig}</span>);
35
+ expect(html).to.equal("<span>test</span>");
36
+ });
37
+
38
+ it("should render computed as Text", async () => {
39
+ const sig = signal("test");
40
+ const comp = computed(() => `${sig} ${sig}`);
41
+ const html = await render(<span>{comp}</span>);
42
+ expect(html).to.equal("<span>test test</span>");
43
+ });
44
+ });
45
+
46
+ describe("mount component bindings", () => {
47
+ it("should mount component with signals as text", async () => {
48
+ const sig = signal("foo");
49
+
50
+ function App() {
51
+ const value = sig.value;
52
+ return <p>{value}</p>;
53
+ }
54
+
55
+ const html = await render(<App />);
56
+ expect(html).to.equal("<p>foo</p>");
57
+ });
58
+
59
+ it("should activate signal accessed in render", async () => {
60
+ const sig = signal(null);
61
+
62
+ function App() {
63
+ const arr = useComputed(() => {
64
+ // trigger read
65
+ sig.value;
66
+
67
+ return [];
68
+ });
69
+
70
+ const str = arr.value.join(", ");
71
+ return <p>{str}</p>;
72
+ }
73
+
74
+ try {
75
+ await render(<App />);
76
+ } catch (e: any) {
77
+ expect.fail(e.stack);
78
+ }
79
+ });
80
+
81
+ it("should properly mount in strict mode", async () => {
82
+ const sig = signal(-1);
83
+
84
+ const Test = () => <p>{sig.value}</p>;
85
+ const App = () => (
86
+ <StrictMode>
87
+ <Test />
88
+ </StrictMode>
89
+ );
90
+
91
+ const html = await render(<App />);
92
+ expect(html).to.equal("<p>-1</p>");
93
+ });
94
+
95
+ it("should correctly mount components that have useReducer()", async () => {
96
+ const count = signal(0);
97
+
98
+ const Test = () => {
99
+ const [state] = useReducer((state: number, action: number) => {
100
+ return state + action;
101
+ }, -2);
102
+
103
+ const doubled = count.value * 2;
104
+
105
+ return (
106
+ <pre>
107
+ <code>{state}</code>
108
+ <code>{doubled}</code>
109
+ </pre>
110
+ );
111
+ };
112
+
113
+ const html = await render(<Test />);
114
+ expect(html).to.equal("<pre><code>-2</code><code>0</code></pre>");
115
+ });
116
+
117
+ it("should not fail when a component calls setState while mounting", async () => {
118
+ function App() {
119
+ const [state, setState] = useState(0);
120
+ if (state == 0) {
121
+ setState(1);
122
+ }
123
+
124
+ return <div>{state}</div>;
125
+ }
126
+
127
+ const html = await render(<App />);
128
+ expect(html).to.equal("<div>1</div>");
129
+ });
130
+
131
+ it("should not fail when a component calls setState multiple times while mounting", async () => {
132
+ function App() {
133
+ const [state, setState] = useState(0);
134
+ if (state < 5) {
135
+ setState(state + 1);
136
+ }
137
+
138
+ return <div>{state}</div>;
139
+ }
140
+
141
+ const html = await render(<App />);
142
+ expect(html).to.equal("<div>5</div>");
143
+ });
144
+ });
145
+
146
+ describe("useSignal()", () => {
147
+ it("should create a signal from a primitive value", async () => {
148
+ function App() {
149
+ const count = useSignal(1);
150
+ return (
151
+ <div>
152
+ {count}
153
+ <button onClick={() => count.value++}>Increment</button>
154
+ </div>
155
+ );
156
+ }
157
+
158
+ const html = await render(<App />);
159
+ expect(html).to.equal("<div>1<button>Increment</button></div>");
160
+ });
161
+
162
+ it("should properly update signal values changed during mount", async () => {
163
+ function App() {
164
+ const count = useSignal(0);
165
+ if (count.value == 0) {
166
+ count.value++;
167
+ }
168
+
169
+ return (
170
+ <div>
171
+ {count}
172
+ <button onClick={() => count.value++}>Increment</button>
173
+ </div>
174
+ );
175
+ }
176
+
177
+ const html = await render(<App />);
178
+ expect(html).to.equal("<div>1<button>Increment</button></div>");
179
+
180
+ const html2 = await render(<App />);
181
+ expect(html2).to.equal("<div>1<button>Increment</button></div>");
182
+ });
183
+ });
184
+ }
@@ -0,0 +1,126 @@
1
+ import React from "react";
2
+ import sinon from "sinon";
3
+ import { act as realAct } from "react-dom/test-utils";
4
+
5
+ export interface Root {
6
+ render(element: JSX.Element | null): void;
7
+ unmount(): void;
8
+ }
9
+
10
+ export const isProd = process.env.NODE_ENV === "production";
11
+ export const isReact16 = React.version.startsWith("16.");
12
+
13
+ // We need to use createRoot() if it's available, but it's only available in
14
+ // React 18. To enable local testing with React 16 & 17, we'll create a fake
15
+ // createRoot() that uses render() and unmountComponentAtNode() instead.
16
+ let createRootCache: ((container: Element) => Root) | undefined;
17
+ export async function createRoot(container: Element): Promise<Root> {
18
+ if (!createRootCache) {
19
+ try {
20
+ // @ts-expect-error ESBuild will replace this import with a require() call
21
+ // if it resolves react-dom/client. If it doesn't, it will leave the
22
+ // import untouched causing a runtime error we'll handle below.
23
+ const { createRoot } = await import("react-dom/client");
24
+ createRootCache = createRoot;
25
+ } catch (e) {
26
+ // @ts-expect-error ESBuild will replace this import with a require() call
27
+ // if it resolves react-dom.
28
+ const { render, unmountComponentAtNode } = await import("react-dom");
29
+ createRootCache = (container: Element) => ({
30
+ render(element: JSX.Element) {
31
+ render(element, container);
32
+ },
33
+ unmount() {
34
+ unmountComponentAtNode(container);
35
+ },
36
+ });
37
+ }
38
+ }
39
+
40
+ return createRootCache(container);
41
+ }
42
+
43
+ // When testing using react's production build, we can't use act (React
44
+ // explicitly throws an error in this situation). So instead we'll fake act by
45
+ // waiting for a requestAnimationFrame and then 10ms for React's concurrent
46
+ // rerendering and any effects to flush. We'll make a best effort to throw a
47
+ // helpful error in afterEach if we detect that act() was called but not
48
+ // awaited.
49
+ const afterFrame = (ms: number) =>
50
+ new Promise(r => requestAnimationFrame(() => setTimeout(r, ms)));
51
+
52
+ let acting = 0;
53
+ async function prodActShim(cb: () => void | Promise<void>): Promise<void> {
54
+ acting++;
55
+ try {
56
+ await cb();
57
+ await afterFrame(10);
58
+ } finally {
59
+ acting--;
60
+ }
61
+ }
62
+
63
+ export function checkHangingAct() {
64
+ if (acting > 0) {
65
+ throw new Error(
66
+ `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.`
67
+ );
68
+ }
69
+ }
70
+
71
+ export const act =
72
+ process.env.NODE_ENV === "production"
73
+ ? (prodActShim as typeof realAct)
74
+ : realAct;
75
+
76
+ /**
77
+ * `console.log` supports formatting strings with `%s` for string substitutions.
78
+ * This function accepts a string and additional arguments of values and returns
79
+ * a string with the values substituted in.
80
+ */
81
+ export function consoleFormat(str: string, ...values: unknown[]): string {
82
+ let idx = 0;
83
+ return str.replace(/%s/g, () => String(values[idx++]));
84
+ }
85
+
86
+ declare global {
87
+ let errorSpy: sinon.SinonSpy | undefined;
88
+ }
89
+
90
+ // Only one spy can be active on an object at a time and since all tests share
91
+ // the same console object we need to make sure we're only spying on it once.
92
+ // We'll use this method to share the spy across all tests.
93
+ export function getConsoleErrorSpy(): sinon.SinonSpy {
94
+ if (typeof errorSpy === "undefined") {
95
+ (globalThis as any).errorSpy = sinon.spy(console, "error");
96
+ }
97
+
98
+ return errorSpy!;
99
+ }
100
+
101
+ const messagesToIgnore = [
102
+ // Ignore errors for timeouts of tests that often happen while debugging
103
+ /async tests and hooks,/,
104
+ // Ignore React 16 warnings about awaiting `act` calls (warning removed in React 18)
105
+ /Do not await the result of calling act/,
106
+ // Ignore how chai or mocha uses `console.error` to print out errors
107
+ /AssertionError/,
108
+ ];
109
+
110
+ export function checkConsoleErrorLogs(): void {
111
+ const errorSpy = getConsoleErrorSpy();
112
+ if (errorSpy.called) {
113
+ let message: string;
114
+ if (errorSpy.firstCall.args[0].toString().includes("%s")) {
115
+ message = consoleFormat(...errorSpy.firstCall.args);
116
+ } else {
117
+ message = errorSpy.firstCall.args.join(" ");
118
+ }
119
+
120
+ if (messagesToIgnore.every(re => re.test(message) === false)) {
121
+ expect.fail(
122
+ `Console.error was unexpectedly called with this message: \n${message}`
123
+ );
124
+ }
125
+ }
126
+ }
package/test/utils.ts DELETED
@@ -1,67 +0,0 @@
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;