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)
})
})