import { render, screen, waitFor } from "@testing-library/react" import { Effect, Layer } from "effect" import { describe, expect, it, vi } from "vitest" import * as Component from "../src/Component.js" import * as ReactRuntime from "../src/ReactRuntime.js" describe("Component", () => { it("runs useOnMount only once across rerenders", async () => { const onMount = vi.fn(() => Effect.succeed("mounted")) const runtime = ReactRuntime.make(Layer.empty) const effectRuntime = await Effect.runPromise(runtime.runtime.runtimeEffect) const Probe = Component.makeUntraced("UseOnMountProbe")(function*() { const value = yield* Component.useOnMount(onMount) return
{value}
}).pipe( Component.withRuntime(runtime.context) ) const view = render( ) await screen.findByText("mounted") expect(onMount).toHaveBeenCalledTimes(1) view.rerender( ) expect(await screen.findByText("mounted")).toBeTruthy() expect(onMount).toHaveBeenCalledTimes(1) view.unmount() await Effect.runPromise(runtime.runtime.disposeEffect) }) it("recomputes useOnChange only when dependencies change", async () => { const onChange = vi.fn((value: number) => Effect.succeed(`value:${value}`)) const runtime = ReactRuntime.make(Layer.empty) const effectRuntime = await Effect.runPromise(runtime.runtime.runtimeEffect) const Probe = Component.makeUntraced("UseOnChangeProbe")(function*(props: { readonly value: number }) { const result = yield* Component.useOnChange(() => onChange(props.value), [props.value], { finalizerExecutionDebounce: 0, }) return
{result}
}).pipe( Component.withRuntime(runtime.context) ) const view = render( ) await screen.findByText("value:1") expect(onChange).toHaveBeenCalledTimes(1) view.rerender( ) expect(await screen.findByText("value:1")).toBeTruthy() expect(onChange).toHaveBeenCalledTimes(1) view.rerender( ) await screen.findByText("value:2") expect(onChange).toHaveBeenCalledTimes(2) view.unmount() await Effect.runPromise(runtime.runtime.disposeEffect) }) it("closes the previous scope on dependency changes and unmount", async () => { const cleanup = vi.fn() const runtime = ReactRuntime.make(Layer.empty) const effectRuntime = await Effect.runPromise(runtime.runtime.runtimeEffect) const Probe = Component.makeUntraced("ScopeCleanupProbe")(function*(props: { readonly value: string }) { const result = yield* Component.useOnChange( () => Effect.gen(function*() { yield* Effect.addFinalizer(() => Effect.sync(() => cleanup(props.value))) return props.value }), [props.value], { finalizerExecutionDebounce: 0 }, ) return
{result}
}).pipe( Component.withRuntime(runtime.context) ) const view = render( ) await screen.findByText("first") expect(cleanup).not.toHaveBeenCalled() view.rerender( ) await screen.findByText("second") await waitFor(() => expect(cleanup).toHaveBeenCalledWith("first")) expect(cleanup).toHaveBeenCalledTimes(1) view.unmount() await waitFor(() => expect(cleanup).toHaveBeenCalledWith("second")) expect(cleanup).toHaveBeenCalledTimes(2) await Effect.runPromise(runtime.runtime.disposeEffect) }) })