Skip to content

Commit

Permalink
Merge commit 'e467e3b124f29759cab0656e35e040dc1ed0ed62' of https://gi…
Browse files Browse the repository at this point in the history
  • Loading branch information
XantreDev committed Aug 4, 2023
2 parents 5875fd8 + e467e3b commit b029329
Show file tree
Hide file tree
Showing 15 changed files with 553 additions and 356 deletions.
23 changes: 12 additions & 11 deletions packages/utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,37 +28,37 @@
"types": "./dist/esm/index.d.ts",
"import": "./dist/esm/index.js",
"react-native": "./src/index.ts",
"main": "./dist/cjs/index.js"
"require": "./dist/cjs/index.js"
},
"./hooks": {
"types": "./dist/esm/hooks/index.d.ts",
"import": "./dist/esm/hooks/index.js",
"react-native": "./src/hooks/index.ts",
"main": "./dist/cjs/hooks/index.js"
"require": "./dist/cjs/hooks/index.js"
},
"./flat-store": {
"types": "./dist/esm/flat-store/index.d.ts",
"react-native": "./src/flat-store/index.ts",
"import": "./dist/esm/flat-store/index.js",
"main": "./dist/cjs/flat-store/index.js"
"require": "./dist/cjs/flat-store/index.js"
},
"./components": {
"react-native": "./src/components/index.ts",
"types": "./dist/esm/components/index.d.ts",
"import": "./dist/esm/components/index.js",
"main": "./dist/cjs/components/index.js"
"require": "./dist/cjs/components/index.js"
},
"./resource": {
"react-native": "./src/resource/index.ts",
"types": "./dist/esm/resource/index.d.ts",
"import": "./dist/esm/resource/index.js",
"main": "./dist/cjs/resource/index.js"
"require": "./dist/cjs/resource/index.js"
},
"./hoc": {
"react-native": "./src/hoc.ts",
"types": "./dist/esm/hoc.d.ts",
"import": "./dist/esm/hoc.js",
"main": "./dist/cjs/hoc.js"
"react-native": "./src/hoc/index.ts",
"types": "./dist/esm/hoc/index.d.ts",
"import": "./dist/esm/hoc/index.js",
"require": "./dist/cjs/hoc/index.js"
}
},
"scripts": {
Expand All @@ -71,8 +71,8 @@
"build": "pnpm clean && concurrently \"pnpm build:esm\" \"pnpm build:cjs\""
},
"peerDependencies": {
"react": "17.*.* || 18.*.*",
"@preact/signals-react": ">=1.2"
"@preact/signals-react": ">=1.2",
"react": "17.*.* || 18.*.*"
},
"peerDependenciesMeta": {
"react": {
Expand All @@ -89,6 +89,7 @@
"concurrently": "^8.2.0",
"happy-dom": "^10.5.2",
"hotscript": "^1.0.13",
"radash": "^11.0.0",
"react": "^18",
"react-dom": "^18",
"type-fest": "^3.12.0"
Expand Down
164 changes: 86 additions & 78 deletions packages/utils/src/__tests__/hoc.test.tsx
Original file line number Diff line number Diff line change
@@ -1,61 +1,65 @@
import { ReadonlySignal, signal } from "@preact-signals/unified-signals";
import React, { PropsWithChildren } from "react";
import { describe, expect, expectTypeOf, it, vi } from "vitest";
import { describe, expectTypeOf, it, vi } from "vitest";
import { $, Uncached } from "../$";
import { ReactiveProps, reactifyProps, signalifyProps } from "../hoc";
import { createRenderer } from "./utils";

describe("signalifyProps()", () => {
const { act, reactRoot, root } = createRenderer();
import { ReactiveProps, reactifyPropsLite, signalifyProps } from "../hoc/hoc";
import { itRenderer } from "./utils";

describe.concurrent("signalifyProps()", () => {
for (const valueType of ["signal", "Uncached"] as const) {
it(`should force rerender dependent component (${valueType})`, () => {
const B = signalifyProps(
vi.fn((props: { value: number }) => <div>{props.value}</div>)
);
itRenderer(
`should force rerender dependent component (${valueType})`,
({ act, reactRoot, expect, root }) => {
const B = signalifyProps(
vi.fn((props: { value: number }) => <div>{props.value}</div>)
);

const sig = signal(10);

act(() => {
reactRoot().render(
<B value={valueType === "signal" ? sig : $(() => sig.value)} />
);
});

expect(B).toHaveBeenCalledTimes(1);
expect(B).toHaveBeenCalledWith({ value: 10 }, {});
expect(root.firstChild).is.instanceOf(HTMLDivElement);
expect(root.firstChild).has.property("textContent", "10");

act(() => {
sig.value = 20;
});

expect(B).toHaveBeenCalledTimes(2);
expect(B).toHaveBeenCalledWith({ value: 20 }, {});
expect(root.firstChild).is.instanceOf(HTMLDivElement);
expect(root.firstChild).has.property("textContent", "20");
}
);
}

itRenderer(
"should not rerender when unread signal changed",
({ expect, act, reactRoot }) => {
const B = signalifyProps(vi.fn((props: { value: number }) => null));

const sig = signal(10);

act(() => {
reactRoot().render(
<B value={valueType === "signal" ? sig : $(() => sig.value)} />
);
reactRoot().render(<B value={$(() => sig.value)} />);
});

expect(B).toHaveBeenCalledTimes(1);
expect(B).toHaveBeenCalledWith({ value: 10 }, {});
expect(root.firstChild).is.instanceOf(HTMLDivElement);
expect(root.firstChild).has.property("textContent", "10");

act(() => {
sig.value = 20;
});

expect(B).toHaveBeenCalledTimes(2);
expect(B).toHaveBeenCalledWith({ value: 20 }, {});
expect(root.firstChild).is.instanceOf(HTMLDivElement);
expect(root.firstChild).has.property("textContent", "20");
});
}

it("should not rerender when unread signal changed", () => {
const B = signalifyProps(vi.fn((props: { value: number }) => null));

const sig = signal(10);

act(() => {
reactRoot().render(<B value={$(() => sig.value)} />);
});

expect(B).toHaveBeenCalledTimes(1);
expect(B).toHaveBeenCalledWith({ value: 10 }, {});

act(() => {
sig.value = 20;
});

expect(B).toHaveBeenCalledOnce();
});
expect(B).toHaveBeenCalledOnce();
}
);

it("should handle types", () => {
const B = signalifyProps((props: PropsWithChildren<{ value: number }>) => (
Expand All @@ -73,9 +77,9 @@ describe("signalifyProps()", () => {
});
});

describe("reactifyProps()", () => {
describe.concurrent("reactifyProps()", () => {
it("should handle explicitly defined reactive props", () => {
const A = reactifyProps((props: ReactiveProps<{ value: number }>) => (
const A = reactifyPropsLite((props: ReactiveProps<{ value: number }>) => (
<div>{props.value}</div>
));

Expand All @@ -86,51 +90,55 @@ describe("reactifyProps()", () => {
});

it("should throw on not explicitly defined reactive props", () => {
const B = reactifyProps((props: { value: number }) => (
const B = reactifyPropsLite((props: { value: number }) => (
<div>{props.value}</div>
));

expectTypeOf(B).parameter(0).toBeNever();
});

const { act, reactRoot, root } = createRenderer();
itRenderer(
"should rerender when read signal changed",
async ({ expect, act, root, reactRoot }) => {
const sig = signal(10);

it("should rerender when read signal changed", async () => {
const sig = signal(10);
const aRender = vi.fn((props: ReactiveProps<{ value: number }>) => (
<div>{props.value}</div>
));
const A = reactifyPropsLite(aRender);
await act(() => reactRoot().render(<A value={sig} />));

const aRender = vi.fn((props: ReactiveProps<{ value: number }>) => (
<div>{props.value}</div>
));
const A = reactifyProps(aRender);
await act(() => reactRoot().render(<A value={sig} />));

expect(A).toHaveBeenCalledTimes(1);
expect(A).toHaveBeenCalledWith({ value: 10 }, {});
expect(root.firstChild).is.instanceOf(HTMLDivElement);
expect(root.firstChild).has.property("textContent", "10");

await act(() => {
sig.value = 20;
});

expect(A).toHaveBeenCalledTimes(2);
expect(A).toHaveBeenCalledWith({ value: 20 }, {});
expect(root.firstChild).is.instanceOf(HTMLDivElement);
expect(root.firstChild).has.property("textContent", "20");
});
it("should not rerender when unread signal changed", async () => {
const sig = signal(10);
expect(A).toHaveBeenCalledTimes(1);
expect(A).toHaveBeenCalledWith({ value: 10 }, {});
expect(root.firstChild).is.instanceOf(HTMLDivElement);
expect(root.firstChild).has.property("textContent", "10");

const aRender = vi.fn((props: ReactiveProps<{ value: number }>) => null);
const A = reactifyProps(aRender);
await act(() => reactRoot().render(<A value={sig} />));
await act(() => {
sig.value = 20;
});

expect(A).toHaveBeenCalledTimes(1);
expect(A).toHaveBeenCalledWith({ value: 10 }, {});
await act(() => {
sig.value = 20;
});
expect(A).toHaveBeenCalledTimes(2);
expect(A).toHaveBeenCalledWith({ value: 20 }, {});
expect(root.firstChild).is.instanceOf(HTMLDivElement);
expect(root.firstChild).has.property("textContent", "20");
}
);
itRenderer(
"should not rerender when unread signal changed",
async ({ act, reactRoot, expect }) => {
const sig = signal(10);

expect(A).toHaveBeenCalledTimes(1);
});
const aRender = vi.fn((props: ReactiveProps<{ value: number }>) => null);
const A = reactifyPropsLite(aRender);
await act(() => reactRoot().render(<A value={sig} />));

expect(A).toHaveBeenCalledTimes(1);
expect(A).toHaveBeenCalledWith({ value: 10 }, {});
await act(() => {
sig.value = 20;
});

expect(A).toHaveBeenCalledTimes(1);
}
);
});
63 changes: 54 additions & 9 deletions packages/utils/src/__tests__/utils.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,35 @@
import { defer, pick } from "radash";
import { createRoot } from "react-dom/client";
import { act } from "react-dom/test-utils";
import { afterEach } from "vitest";
import { act as reactAct } from "react-dom/test-utils";
import { TestFunction, afterEach, it } from "vitest";

const raf = (): Promise<void> =>
new Promise((resolve) => requestAnimationFrame(() => resolve()));

const act = async (callback: () => unknown): Promise<void> => {
const res = reactAct(callback);

await res;
await raf();
await res;
};

export const sleep = (ms: number) =>
new Promise((resolve) => setTimeout(resolve, ms));

export const createRenderer = () => {
export const _createRenderer = () => {
const root = (() => {
const el = document.createElement("div");
document.body.appendChild(el);
return el;
})();
let reactRoot = createRoot(root);

afterEach(async () => {
reactRoot.unmount();
await sleep(0);
const dispose = async () => {
await act(() => {
reactRoot.unmount();
});
root.innerHTML = "";
reactRoot = createRoot(root);
});
};
return {
root,
reactRoot: () => ({
Expand All @@ -27,6 +38,40 @@ export const createRenderer = () => {
await act(() => reactRoot.render(data));
},
}),
recreateRoot: () => {
reactRoot = createRoot(root);
},
dispose,
act,
};
};

export const createRenderer = () => {
const renderer = _createRenderer();

afterEach(async () => {
await renderer.dispose();
renderer.recreateRoot();
});
return pick(renderer, ["root", "reactRoot", "act"]);
};

type WithRendererProps = ReturnType<typeof createRenderer> &
Pick<Parameters<TestFunction<unknown>>[0], "expect">;

export const withRenderer =
(callback: (renderer: WithRendererProps) => unknown) =>
async (arg: Parameters<TestFunction<unknown>>[0]) =>
await defer(async (cleanup) => {
const renderer = _createRenderer();
cleanup(() => renderer.dispose(), { rethrow: true });
return await callback({
expect: arg.expect,
...pick(renderer, ["act", "reactRoot", "root"]),
});
});

export const itRenderer = (
name: string,
callback: (props: WithRendererProps) => unknown
) => it(name, withRenderer(callback));
13 changes: 6 additions & 7 deletions packages/utils/src/components/__tests__/Computed.test.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import { signal } from "@preact-signals/unified-signals";
import { describe, expect, it, vi } from "vitest";
import { createRenderer } from "../../__tests__/utils";
import React from "react";
import { describe, vi } from "vitest";
import { itRenderer } from "../../__tests__/utils";
import { Computed } from "../components/Computed";

describe("Computed()", () => {
const { reactRoot, act, root } = createRenderer();

it("should render", async () => {
describe.concurrent("Computed()", () => {
itRenderer("should render", async ({ expect, reactRoot, root }) => {
await reactRoot().render(<Computed>{() => 10}</Computed>);

const content = root.firstChild;
expect(content).is.instanceOf(Text);
expect(content).has.property("data", "10");
});

it("should be reactive", async () => {
itRenderer("should be reactive", async ({ expect, act, reactRoot, root }) => {
const multiplier = signal(1);
const compFn = vi.fn(() => 10 * multiplier.value);
await reactRoot().render(<Computed>{compFn}</Computed>);
Expand Down
1 change: 1 addition & 0 deletions packages/utils/src/components/__tests__/For.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { signal } from "@preact-signals/unified-signals";
import React from "react";
import { SpyInstance, describe, expect, it, vi } from "vitest";
import { createRenderer } from "../../__tests__/utils";
import { For } from "../components/For";
Expand Down
Loading

0 comments on commit b029329

Please sign in to comment.