diff --git a/test/connectFactoryObservable.test.tsx b/test/connectFactoryObservable.test.tsx index 0853ceb..ecfd23a 100644 --- a/test/connectFactoryObservable.test.tsx +++ b/test/connectFactoryObservable.test.tsx @@ -1,6 +1,10 @@ import { connectFactoryObservable } from "../src" -import { from, of, defer, concat, BehaviorSubject } from "rxjs" -import { renderHook, act } from "@testing-library/react-hooks" +import { from, of, defer, concat, BehaviorSubject, throwError } from "rxjs" +import { renderHook, act as actHook } from "@testing-library/react-hooks" +import { render, act } from "@testing-library/react" +import { map, switchMap } from "rxjs/operators" +import { Component, ErrorInfo, FC } from "react" +import React from "react" const wait = (ms: number) => new Promise(res => setTimeout(res, ms)) @@ -8,7 +12,10 @@ describe("connectFactoryObservable", () => { const originalError = console.error beforeAll(() => { console.error = (...args: any) => { - if (/Consider adding an error/.test(args[0])) { + if ( + /Uncaught 'controlled error'/.test(args[0]) || + /using the error boundary .* TestErrorBoundary/.test(args[0]) + ) { return } originalError.call(console, ...args) @@ -20,9 +27,46 @@ describe("connectFactoryObservable", () => { }) describe("hook", () => { it("returns the latest emitted value", async () => { - const [useNumber] = connectFactoryObservable((id: number) => of(id)) - const { result } = renderHook(() => useNumber(1)) + const valueStream = new BehaviorSubject(1) + const [useNumber] = connectFactoryObservable(() => valueStream) + const { result } = renderHook(() => useNumber()) expect(result.current).toBe(1) + + actHook(() => { + valueStream.next(3) + }) + expect(result.current).toBe(3) + }) + + it("shares the multicasted subscription with all of the components that use the same parameters", async () => { + let subscriberCount = 0 + const observable$ = defer(() => { + subscriberCount += 1 + return from([1, 2, 3, 4, 5]) + }) + + const [ + useLatestNumber, + latestNumber$, + ] = connectFactoryObservable((id: number, value: number) => + concat(observable$, of(id + value)), + ) + expect(subscriberCount).toBe(0) + + renderHook(() => useLatestNumber(1, 1)) + expect(subscriberCount).toBe(1) + + renderHook(() => useLatestNumber(1, 1)) + expect(subscriberCount).toBe(1) + + latestNumber$(1, 1).subscribe() + expect(subscriberCount).toBe(1) + + renderHook(() => useLatestNumber(1, 2)) + expect(subscriberCount).toBe(2) + + renderHook(() => useLatestNumber(2, 2)) + expect(subscriberCount).toBe(3) }) it("shares the source subscription until the refCount has stayed at zero for the grace-period", async () => { @@ -60,27 +104,62 @@ describe("connectFactoryObservable", () => { const errStream = new BehaviorSubject(1) const [useError] = connectFactoryObservable(() => errStream) - renderHook(() => useError()) + const ErrorComponent = () => { + const value = useError() - expect(() => - act(() => { - errStream.error("error") - }), - ).toThrow() + return <>{value} + } + + const errorCallback = jest.fn() + render( + + + , + ) + + act(() => { + errStream.error("controlled error") + }) + + expect(errorCallback).toHaveBeenCalledWith( + "controlled error", + expect.any(Object), + ) }) it("doesn't throw errors on components that will get unmounted on the next cycle", () => { - const errStream = new BehaviorSubject(1) - const [useError] = connectFactoryObservable(() => errStream) + const valueStream = new BehaviorSubject(1) + const [useValue, value$] = connectFactoryObservable(() => valueStream) + const [useError] = connectFactoryObservable(() => + value$().pipe(switchMap(v => (v === 1 ? of(v) : throwError("error")))), + ) - const { unmount } = renderHook(() => useError()) + const ErrorComponent: FC = () => { + const value = useError() - expect(() => - act(() => { - errStream.error("error") - unmount() - }), - ).not.toThrow() + return <>{value} + } + + const Container: FC = () => { + const value = useValue() + + return value === 1 ? : <>Nothing to show here + } + + const errorCallback = jest.fn() + render( + + + + + , + ) + + act(() => { + valueStream.next(2) + }) + + expect(errorCallback).not.toHaveBeenCalled() }) }) describe("observable", () => { @@ -127,3 +206,32 @@ describe("connectFactoryObservable", () => { }) }) }) + +class TestErrorBoundary extends Component< + { + onError: (error: Error, errorInfo: ErrorInfo) => void + }, + { + hasError: boolean + } +> { + state = { + hasError: false, + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + this.props.onError(error, errorInfo) + } + + static getDerivedStateFromError() { + return { hasError: true } + } + + render() { + if (this.state.hasError) { + return "error" + } + + return this.props.children + } +}