diff --git a/src/data/createAsync.ts b/src/data/createAsync.ts index deb6a4999..67f21634f 100644 --- a/src/data/createAsync.ts +++ b/src/data/createAsync.ts @@ -1,7 +1,14 @@ /** * This is mock of the eventual Solid 2.0 primitive. It is not fully featured. */ -import { type Accessor, createResource, sharedConfig, type Setter, untrack } from "solid-js"; +import { + type Accessor, + createResource, + sharedConfig, + type Setter, + untrack, + catchError +} from "solid-js"; import { createStore, reconcile, type ReconcileOptions, unwrap } from "solid-js/store"; import { isServer } from "solid-js/web"; @@ -13,7 +20,7 @@ import { isServer } from "solid-js/web"; export type AccessorWithLatest = { (): T; latest: T; -} +}; export function createAsync( fn: (prev: T) => Promise, @@ -40,19 +47,28 @@ export function createAsync( } ): AccessorWithLatest { let resource: () => T; - let prev = () => !resource || (resource as any).state === "unresolved" ? undefined : (resource as any).latest; + let prev = () => + !resource || (resource as any).state === "unresolved" ? undefined : (resource as any).latest; + [resource] = createResource( - () => subFetch(fn, untrack(prev)), + () => + subFetch( + fn, + catchError( + () => untrack(prev), + () => undefined + ) + ), v => v, options as any ); const resultAccessor: AccessorWithLatest = (() => resource()) as any; - Object.defineProperty(resultAccessor, 'latest', { + Object.defineProperty(resultAccessor, "latest", { get() { return (resource as any).latest; } - }) + }); return resultAccessor; } @@ -85,9 +101,20 @@ export function createAsyncStore( } = {} ): AccessorWithLatest { let resource: () => T; - let prev = () => !resource || (resource as any).state === "unresolved" ? undefined : unwrap((resource as any).latest); + + let prev = () => + !resource || (resource as any).state === "unresolved" + ? undefined + : unwrap((resource as any).latest); [resource] = createResource( - () => subFetch(fn, untrack(prev)), + () => + subFetch( + fn, + catchError( + () => untrack(prev), + () => undefined + ) + ), v => v, { ...options, @@ -96,11 +123,11 @@ export function createAsyncStore( ); const resultAccessor: AccessorWithLatest = (() => resource()) as any; - Object.defineProperty(resultAccessor, 'latest', { + Object.defineProperty(resultAccessor, "latest", { get() { return (resource as any).latest; } - }) + }); return resultAccessor; } diff --git a/test/data.spec.tsx b/test/data.spec.tsx new file mode 100644 index 000000000..9869f4e6b --- /dev/null +++ b/test/data.spec.tsx @@ -0,0 +1,156 @@ +import { + ErrorBoundary, + ParentProps, + Suspense, + catchError, + createRoot, + createSignal +} from "solid-js"; +import { render } from "solid-js/web"; +import { createAsync, createAsyncStore } from "../src/data"; +import { awaitPromise, waitFor } from "./helpers"; + +function Parent(props: ParentProps) { + return }>{props.children}; +} + +async function getText(arg?: string) { + return arg || "fallback"; +} +async function getError(arg?: any): Promise { + throw Error("error"); +} + +describe("createAsync should", () => { + test("return 'fallback'", () => { + createRoot(() => { + const data = createAsync(() => getText()); + setTimeout(() => expect(data()).toBe("fallback"), 1); + }); + }); + test("return 'text'", () => { + createRoot(() => { + const data = createAsync(() => getText("text")); + setTimeout(() => expect(data()).toBe("text"), 1); + }); + }); + test("initial error to be caught ", () => { + createRoot(() => { + const data = createAsync(() => getError()); + setTimeout(() => catchError(data, err => expect(err).toBeInstanceOf(Error)), 1); + }); + }); + test("catch error after arg change - initial valid", () => + createRoot(async dispose => { + async function throwWhenError(arg: string): Promise { + if (arg === "error") throw new Error("error"); + return arg; + } + + const [arg, setArg] = createSignal(""); + function Child() { + const data = createAsync(() => throwWhenError(arg())); + + return ( +
+ ( +
+
+ )} + > + +

{data()}

+

{data.latest}

+
+
+
+ ); + } + await render( + () => ( + + + + ), + document.body + ); + const childErrorElement = () => document.getElementById("childError"); + const parentErrorElement = document.getElementById("parentError"); + expect(childErrorElement()).toBeNull(); + expect(parentErrorElement).toBeNull(); + setArg("error"); + await awaitPromise(); + + // after changing the arg the error should still be caught by the Child's ErrorBoundary + expect(childErrorElement()).not.toBeNull(); + expect(parentErrorElement).toBeNull(); + + //reset ErrorBoundary + document.getElementById("reset")?.click(); + + expect(childErrorElement()).toBeNull(); + await awaitPromise(); + const dataEl = () => document.getElementById("data"); + + expect(dataEl()).not.toBeNull(); + expect(document.getElementById("data")?.innerHTML).toBe("true"); + expect(document.getElementById("latest")?.innerHTML).toBe("true"); + + document.body.innerHTML = ""; + dispose(); + })); + test("catch consecutive error after initial error change to be caught after arg change", () => + createRoot(async cleanup => { + const [arg, setArg] = createSignal("error"); + function Child() { + const data = createAsync(() => getError(arg())); + + return ( +
+ ( +
+
+ )} + > + {data()} +
+
+ ); + } + await render( + () => ( + + + + ), + document.body + ); + + // Child's ErrorBoundary should catch the error + expect(document.getElementById("childError")).not.toBeNull(); + expect(document.getElementById("parentError")).toBeNull(); + setArg("error_2"); + await awaitPromise(); + // after changing the arg the error should still be caught by the Child's ErrorBoundary + expect(document.getElementById("childError")).not.toBeNull(); + expect(document.getElementById("parentError")).toBeNull(); + + document.getElementById("reset")?.click(); + await awaitPromise(); + expect(document.getElementById("childError")).not.toBeNull(); + expect(document.getElementById("parentError")).toBeNull(); + + document.body.innerHTML = ""; + cleanup(); + })); +}); diff --git a/test/helpers.ts b/test/helpers.ts index 8f2fdc401..26f652497 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -23,3 +23,7 @@ export function createAsyncRoot(fn: (resolve: () => void, disposer: () => void) createRoot(disposer => fn(resolve, disposer)); }); } + +export async function awaitPromise() { + return new Promise(resolve => setTimeout(resolve, 100)); +} diff --git a/test/tsconfig.json b/test/tsconfig.json new file mode 100644 index 000000000..06cefca04 --- /dev/null +++ b/test/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "./../tsconfig.json", + "include": ["."] +} diff --git a/tsconfig.json b/tsconfig.json index e6bd1866e..022a057fa 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,8 +10,6 @@ "outDir": "./dist", "module": "esnext" }, - "include": [ - "./src" - ], + "include": ["./src"], "exclude": ["node_modules/"] -} \ No newline at end of file +}