|
|
|
@@ -0,0 +1,133 @@
|
|
|
|
|
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 <div>{value}</div>
|
|
|
|
|
}).pipe(
|
|
|
|
|
Component.withRuntime(runtime.context)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const view = render(
|
|
|
|
|
<runtime.context.Provider value={effectRuntime}>
|
|
|
|
|
<Probe />
|
|
|
|
|
</runtime.context.Provider>
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
await screen.findByText("mounted")
|
|
|
|
|
expect(onMount).toHaveBeenCalledTimes(1)
|
|
|
|
|
|
|
|
|
|
view.rerender(
|
|
|
|
|
<runtime.context.Provider value={effectRuntime}>
|
|
|
|
|
<Probe />
|
|
|
|
|
</runtime.context.Provider>
|
|
|
|
|
)
|
|
|
|
|
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 <div>{result}</div>
|
|
|
|
|
}).pipe(
|
|
|
|
|
Component.withRuntime(runtime.context)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const view = render(
|
|
|
|
|
<runtime.context.Provider value={effectRuntime}>
|
|
|
|
|
<Probe value={1} />
|
|
|
|
|
</runtime.context.Provider>
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
await screen.findByText("value:1")
|
|
|
|
|
expect(onChange).toHaveBeenCalledTimes(1)
|
|
|
|
|
|
|
|
|
|
view.rerender(
|
|
|
|
|
<runtime.context.Provider value={effectRuntime}>
|
|
|
|
|
<Probe value={1} />
|
|
|
|
|
</runtime.context.Provider>
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
expect(await screen.findByText("value:1")).toBeTruthy()
|
|
|
|
|
expect(onChange).toHaveBeenCalledTimes(1)
|
|
|
|
|
|
|
|
|
|
view.rerender(
|
|
|
|
|
<runtime.context.Provider value={effectRuntime}>
|
|
|
|
|
<Probe value={2} />
|
|
|
|
|
</runtime.context.Provider>
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
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 <div>{result}</div>
|
|
|
|
|
}).pipe(
|
|
|
|
|
Component.withRuntime(runtime.context)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const view = render(
|
|
|
|
|
<runtime.context.Provider value={effectRuntime}>
|
|
|
|
|
<Probe value="first" />
|
|
|
|
|
</runtime.context.Provider>
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
await screen.findByText("first")
|
|
|
|
|
expect(cleanup).not.toHaveBeenCalled()
|
|
|
|
|
|
|
|
|
|
view.rerender(
|
|
|
|
|
<runtime.context.Provider value={effectRuntime}>
|
|
|
|
|
<Probe value="second" />
|
|
|
|
|
</runtime.context.Provider>
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
})
|
|
|
|
|
})
|