import { render, screen, waitFor } from "@testing-library/react"; import { Event } from "cesium"; import type { ReactNode } from "react"; import { createRef, StrictMode } from "react"; import { beforeEach, describe, expect, it, vi, vitest } from "vitest"; import type { CesiumComponentRef } from "./component"; import { createCesiumComponent } from "./component"; import { Provider } from "./context"; beforeEach(() => { console.warn = vi.fn(); }); describe("core/component", () => { it("should create and expose cesium element correctly on initialized", async () => { const create = vi.fn(() => "foobar"); const value = { hoge: 1 } as any; const Component = createCesiumComponent({ name: "test", create, }); const ref = createRef>(); render( , ); await waitFor(() => { expect(create).toBeCalledWith(value, { test: 1 }, null); expect(create).toBeCalledTimes(1); expect(ref.current?.cesiumElement).toBe("foobar"); }); }); it("should create and expose cesium element correctly on initialized asynchronously", async () => { const create = vi.fn(() => Promise.resolve("foobar")); const value = { hoge: 1 } as any; const Component = createCesiumComponent({ name: "test", create, }); const ref = createRef>(); render( , ); await waitFor(() => { expect(ref.current?.cesiumElement).toBe("foobar"); }); expect(create).toBeCalledWith(value, { test: 1 }, null); expect(create).toBeCalledTimes(1); }); it("should call destroy fn on unmounted", () => { const destroy = vi.fn(); const value = { hoge: 1 } as any; const Component = createCesiumComponent({ name: "test", create: () => "foobar", destroy, }); render( , ).unmount(); waitFor(() => { expect(destroy).toBeCalledWith("foobar", value, null, undefined); expect(destroy).toBeCalledTimes(1); }); }); it("should set cesium events after created", async () => { const cesiumElement = { hoge: new Event(), }; const Component = createCesiumComponent void }>({ name: "test", create: () => cesiumElement, cesiumEventProps: { bar: "hoge" }, }); render( {}} />); await waitFor(() => { expect(cesiumElement.hoge.numberOfListeners).toBe(1); }); }); it("should set cesium props after created", async () => { const cesiumElement = { foo: 0, }; const Component = createCesiumComponent({ name: "test", create: () => cesiumElement, cesiumProps: ["foo"], setCesiumPropsAfterCreate: true, }); render(); await waitFor(() => { expect(cesiumElement.foo).toBe(10); }); }); it("should re-attach cesium events after a StrictMode remount with setCesiumPropsAfterCreate", async () => { // Regression test for #736 / #680: under StrictMode the component is mounted, // unmounted, then remounted. For setCesiumPropsAfterCreate components the // second mount goes through updateProperties(initialProps), which diffs // against prevProps. If prevProps is not reset on unmount, the stable event // handler reference is treated as unchanged and the listener is never // re-attached, leaving numberOfListeners at 0. const cesiumElement = { hoge: new Event(), }; // A stable handler reference shared across mounts (as in real usage where // the same callback prop is passed on every render). const handler = () => {}; const Component = createCesiumComponent void }>({ name: "test", create: () => cesiumElement, cesiumEventProps: { bar: "hoge" }, setCesiumPropsAfterCreate: true, }); render( , ); await waitFor(() => { expect(cesiumElement.hoge.numberOfListeners).toBe(1); }); }); it("should update cesium props", async () => { const cesiumElement = { foo: 0, }; const Component = createCesiumComponent({ name: "test", create: () => cesiumElement, cesiumProps: ["foo"], }); const { rerender } = render(); await waitFor(() => { expect(cesiumElement.foo).toBe(0); }); rerender(); await waitFor(() => { expect(cesiumElement.foo).toBe(1); }); }); it("should update cesium events", async () => { const cesiumElement = { foo: new Event(), bar: new Event(), hoge: new Event(), }; const Component = createCesiumComponent< typeof cesiumElement, { foo?: () => void; bar?: () => void; hoge?: () => void } >({ name: "test", create: () => cesiumElement, cesiumEventProps: { foo: "foo", bar: "bar", hoge: "hoge", }, }); const { rerender } = render( {}} hoge={() => {}} />); await waitFor(() => { expect(cesiumElement.foo.numberOfListeners).toBe(1); expect(cesiumElement.bar.numberOfListeners).toBe(0); expect(cesiumElement.hoge.numberOfListeners).toBe(1); }); rerender( {}} hoge={() => {}} />); await waitFor(() => { expect(cesiumElement.foo.numberOfListeners).toBe(0); expect(cesiumElement.bar.numberOfListeners).toBe(1); expect(cesiumElement.hoge.numberOfListeners).toBe(1); // TODO }); }); it("should remount when cesium read only props are updated", async () => { const cesiumElement = { foo: 0, }; const createFn = vi.fn((_ctx, props: { foo?: number }) => { if (typeof props.foo === "number") { cesiumElement.foo = props.foo; } return cesiumElement; }); const destroyFn = vi.fn(); const Component = createCesiumComponent({ name: "test", create: createFn, destroy: destroyFn, cesiumReadonlyProps: ["foo"], }); const { rerender } = render(); await waitFor(() => { expect(createFn).toBeCalledTimes(1); expect(destroyFn).toBeCalledTimes(0); expect(cesiumElement.foo).toBe(1); }); rerender(); waitFor(() => { expect(createFn).toBeCalledTimes(2); expect(destroyFn).toBeCalledTimes(1); expect(cesiumElement.foo).toBe(2); }); }); it("should call update", async () => { const updateFn = vi.fn(); const Component = createCesiumComponent<{ hoge: "hoge" }, { foo?: number }>({ name: "test", create: () => ({ hoge: "hoge" }), update: updateFn, }); const { rerender } = render(); await waitFor(() => { expect(updateFn).toBeCalledTimes(0); }); rerender(); await waitFor(() => { expect(updateFn).toBeCalledTimes(1); expect(updateFn).toBeCalledWith({ hoge: "hoge", foo: 1 }, { foo: 1 }, {}, {}); }); }); it("should provide context", async () => { const create1 = vi.fn(() => "test"); const create2 = vi.fn(() => "test"); const Component1 = createCesiumComponent({ name: "test", create: create1, provide: (): any => ({ context: "b" }), }); const Component2 = createCesiumComponent({ name: "test2", create: create2, }); render( , ); await waitFor(() => { expect(create1).toBeCalledWith({ context: "a", context2: "foo" }, expect.anything(), null); expect(create2).toBeCalledWith({ context: "b", context2: "foo" }, expect.anything(), null); }); }); it("should recreate children with the new context when parent is recreated by a readonly prop change", async () => { let n = 0; const create1 = vi.fn(() => `parent${n++}`); const create2 = vi.fn(() => "child"); const destroy2 = vi.fn(); const Parent = createCesiumComponent({ name: "parent", create: create1, cesiumReadonlyProps: ["recreate"], provide: (element): any => ({ parent: element }), }); const Child = createCesiumComponent>({ name: "child", create: create2, destroy: destroy2, }); const { rerender } = render( , ); await waitFor(() => { expect(create1).toBeCalledTimes(1); expect(create2).toHaveBeenLastCalledWith({ parent: "parent0" }, expect.anything(), null); }); // Changing a readonly prop recreates the parent, which provides a new // context (a new `parent` element). Children must tear down and re-create // against it (issues #661 and #685) — exactly once, not repeatedly. rerender( , ); await waitFor(() => { expect(create1).toBeCalledTimes(2); expect(create2).toBeCalledTimes(2); expect(destroy2).toBeCalledTimes(1); expect(create2).toHaveBeenLastCalledWith({ parent: "parent1" }, expect.anything(), null); }); }); it("should invoke onUpdate event when being dirty", () => { const cesiumElement = { foo: 0, }; const onUpdate = vitest.fn(); const Component = createCesiumComponent({ name: "test", create: () => cesiumElement, cesiumProps: ["foo"], }); const { rerender } = render( , ); expect(cesiumElement.foo).toBe(0); expect(onUpdate).not.toBeCalled(); rerender( , ); waitFor(() => { expect(cesiumElement.foo).toBe(1); expect(onUpdate).toBeCalledTimes(1); }); }); it("should render container", async () => { const createFn = vi.fn(() => "foobar"); const Component = createCesiumComponent({ name: "test", create: createFn, renderContainer: true, containerProps: ["className"], }); render( , ); await waitFor(() => { expect(createFn).toBeCalledWith( expect.anything(), expect.anything(), expect.any(HTMLDivElement), ); const containers = screen.getAllByTestId("resium-container"); expect(containers.length).toBe(1); expect(containers[0].getAttribute("class")).toBe("hoge"); }); }); it("should keep state", () => { const provideFn = vi.fn(); const destroyFn = vi.fn(); const state = {}; const Component = createCesiumComponent({ name: "test", create: () => ["foobar", state], provide: provideFn, destroy: destroyFn, }); render( , ).unmount(); waitFor(() => { expect(provideFn).toBeCalledWith( expect.anything(), expect.anything(), expect.anything(), state, ); expect(destroyFn).toBeCalledWith(expect.anything(), expect.anything(), null, state); }); }); it("should not render when noChildren is true", async () => { const Component = createCesiumComponent({ name: "test", noChildren: false, }); const { rerender } = render(

Hello!

, ); await waitFor(() => { expect(screen.getByTestId("hello")).toBeInTheDocument(); }); const Component2 = createCesiumComponent({ name: "test", noChildren: true, }); rerender(

Hello!

, ); await waitFor(() => { expect(screen.queryByTestId("hello")).not.toBeInTheDocument(); }); }); });