diff --git a/packages/uix-host-react/src/components/Extensible.test.tsx b/packages/uix-host-react/src/components/Extensible.test.tsx index e0c46f9..b40c752 100644 --- a/packages/uix-host-react/src/components/Extensible.test.tsx +++ b/packages/uix-host-react/src/components/Extensible.test.tsx @@ -104,7 +104,7 @@ describe("Extensible", () => { }); }); - it("should not call unload if host was never created", () => { + it("should still create a host when extensions resolve empty, and unload it on unmount", async () => { const extensionsProvider = jest.fn().mockResolvedValue({}); const { unmount } = render( @@ -113,11 +113,74 @@ describe("Extensible", () => { ); + // The host must exist even with zero extensions, so consumers relying on + // host readiness (e.g. useHost/loading state) aren't stuck waiting forever. + await waitFor(() => { + expect(MockedHost).toHaveBeenCalled(); + }); + + // load() must not be called since there is nothing to load + expect(mockLoad).not.toHaveBeenCalled(); + + unmount(); + + await waitFor(() => { + expect(mockUnload).toHaveBeenCalled(); + }); + }); + + it("should not call unload if the extensions fetch never resolved before unmount", () => { + const extensionsProvider = jest + .fn() + .mockReturnValue(new Promise(() => {})); + + const { unmount } = render( + +
Test Child
+
+ ); + unmount(); // Unload should not be called if host was never created expect(mockUnload).not.toHaveBeenCalled(); }); + + it("reuses the host (does not recreate) once extensions arrive after an initial empty resolve", async () => { + const emptyProvider = jest.fn().mockResolvedValue({}); + const nonEmptyExtensions: InstalledExtensions = { + "ext-1": { id: "ext-1", url: "https://example.com/ext1" }, + }; + const nonEmptyProvider = jest.fn().mockResolvedValue(nonEmptyExtensions); + + const { rerender } = render( + +
Test Child
+
+ ); + + // Host is created immediately even though there is nothing to load yet. + await waitFor(() => { + expect(MockedHost).toHaveBeenCalledTimes(1); + }); + expect(mockLoad).not.toHaveBeenCalled(); + + // Extensions arrive later (e.g. a slower registry response, or a + // subsequent provider swap). The existing host must be reused, not + // torn down and recreated, and load() should now be called. + rerender( + +
Test Child
+
+ ); + + await waitFor(() => { + expect(mockLoad).toHaveBeenCalledWith(nonEmptyExtensions, undefined); + }); + + expect(MockedHost).toHaveBeenCalledTimes(1); + expect(mockUnload).not.toHaveBeenCalled(); + }); }); describe("Old host unloading when creating new host", () => { diff --git a/packages/uix-host-react/src/components/Extensible.tsx b/packages/uix-host-react/src/components/Extensible.tsx index c60498b..5bc58d6 100644 --- a/packages/uix-host-react/src/components/Extensible.tsx +++ b/packages/uix-host-react/src/components/Extensible.tsx @@ -188,11 +188,16 @@ export function Extensible({ }; } - if (!extensions || !Object.keys(extensions).length) { + if (!extensionListFetched) { return; } + const hasExtensions = !!extensions && !!Object.keys(extensions).length; + const loadExtensions = (hostInstance: Host) => { + if (!hasExtensions) { + return; + } hostInstance .load(extensions, guestOptions) .catch(logError("Load of extensions failed!")); @@ -220,7 +225,7 @@ export function Extensible({ } else { loadExtensions(host); } - }, [debug, hostName, runtimeContainer, extensions]); + }, [debug, hostName, runtimeContainer, extensions, extensionListFetched]); const contextValue = useMemo( () => ({ host, extensionListFetched }),