diff --git a/package.json b/package.json index da14cbd4f..396adbcd1 100644 --- a/package.json +++ b/package.json @@ -273,7 +273,7 @@ "fuse.js": "7.1.0", "himalaya": "1.1.1", "i18next": "25.5.2", - "jotai": "2.14.0", + "jotai": "3.0.0", "lodash-es": "4.18.1", "lucide-react": "1.43.0", "motion": "13.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6fa02d969..32a607068 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -171,8 +171,8 @@ importers: specifier: 25.5.2 version: 25.5.2(typescript@5.9.3) jotai: - specifier: 2.14.0 - version: 2.14.0(@babel/template@7.29.7)(@types/react@19.2.7)(react@19.2.7) + specifier: 3.0.0 + version: 3.0.0(@types/react@19.2.7)(react@19.2.7) lodash-es: specifier: 4.18.1 version: 4.18.1 @@ -4486,19 +4486,13 @@ packages: jose@5.10.0: resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} - jotai@2.14.0: - resolution: {integrity: sha512-JQkNkTnqjk1BlSUjHfXi+pGG/573bVN104gp6CymhrWDseZGDReTNniWrLhJ+zXbM6pH+82+UNJ2vwYQUkQMWQ==} - engines: {node: '>=12.20.0'} + jotai@3.0.0: + resolution: {integrity: sha512-KxhbmsJUp/tmrv6aZgO+nzB3H9K8C0xmk5i3aGkwy/EANg73DVBkggv3zVHaAi5PlNlQgTgP4uiVepo+JC+wSA==} + engines: {node: '>=22.12.0'} peerDependencies: - '@babel/core': '>=7.0.0' - '@babel/template': '>=7.0.0' - '@types/react': '>=17.0.0' - react: '>=17.0.0' + '@types/react': '>=18.0.0' + react: '>=18.0.0' peerDependenciesMeta: - '@babel/core': - optional: true - '@babel/template': - optional: true '@types/react': optional: true react: @@ -10195,9 +10189,8 @@ snapshots: jose@5.10.0: {} - jotai@2.14.0(@babel/template@7.29.7)(@types/react@19.2.7)(react@19.2.7): + jotai@3.0.0(@types/react@19.2.7)(react@19.2.7): optionalDependencies: - '@babel/template': 7.29.7 '@types/react': 19.2.7 react: 19.2.7 diff --git a/src/builder/hooks/use-block-selection-query-sync.test.tsx b/src/builder/hooks/use-block-selection-query-sync.test.tsx index 8ba92761c..5934a8923 100644 --- a/src/builder/hooks/use-block-selection-query-sync.test.tsx +++ b/src/builder/hooks/use-block-selection-query-sync.test.tsx @@ -1,9 +1,8 @@ /** * @vitest-environment happy-dom */ -import { act, renderHook } from "@testing-library/react"; -import { Provider, WritableAtom } from "jotai"; -import { useHydrateAtoms } from "jotai/utils"; +import { act, renderHook, waitFor } from "@testing-library/react"; +import { createStore, Provider, WritableAtom } from "jotai"; import React from "react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { presentBlocksAtom } from "~/builder/atoms/blocks"; @@ -13,22 +12,19 @@ import { useBlockSelectionQuerySync, } from "~/builder/hooks/use-block-selection-query-sync"; import { isPageLoadedAtom } from "~/builder/hooks/use-is-page-loaded"; -import { selectedBlockIdsAtom, useSelectedBlockIds } from "~/builder/hooks/use-selected-blockIds"; +import { selectedBlockIdsAtom } from "~/builder/hooks/use-selected-blockIds"; import { ChaiBlock } from "~/types/common"; type AtomTuple = [WritableAtom, any]; -const HydrateAtoms = ({ initialValues, children }: { initialValues: AtomTuple[]; children: React.ReactNode }) => { - useHydrateAtoms(initialValues); - return children; +const createTestStore = (initialValues: AtomTuple[]) => { + const store = createStore(); + initialValues.forEach(([atom, value]) => { + store.set(atom, value); + }); + return store; }; -const TestProvider = ({ initialValues, children }: { initialValues: AtomTuple[]; children: React.ReactNode }) => ( - - {children} - -); - const blocks: ChaiBlock[] = [ { _id: "block-1", _type: "Box" }, { _id: "block-2", _type: "Box", _parent: "block-1" }, @@ -89,136 +85,89 @@ describe("useBlockSelectionQuerySync", () => { replaceStateSpy.mockRestore(); }); - it("should preselect block from URL on page load", () => { + it("should preselect block from URL on page load", async () => { window.history.replaceState({}, "", "/?bid=block-2"); + const store = createTestStore([ + [presentBlocksAtom, blocks], + [isPageLoadedAtom, true], + ]); + + renderHook(() => useBlockSelectionQuerySync(), { + wrapper: ({ children }) => {children}, + }); - const { result } = renderHook( - () => { - useBlockSelectionQuerySync(); - return useSelectedBlockIds(); - }, - { - wrapper: ({ children }) => ( - - {children} - - ), - }, - ); - - expect(result.current[0]).toEqual(["block-2"]); + await waitFor(() => { + expect(store.get(selectedBlockIdsAtom)).toEqual(["block-2"]); + }); }); it("should clear stale block param if block does not exist", () => { window.history.replaceState({}, "", "/?bid=non-existent"); + const store = createTestStore([ + [presentBlocksAtom, blocks], + [isPageLoadedAtom, true], + ]); - renderHook( - () => { - useBlockSelectionQuerySync(); - return useSelectedBlockIds(); - }, - { - wrapper: ({ children }) => ( - - {children} - - ), - }, - ); + renderHook(() => useBlockSelectionQuerySync(), { + wrapper: ({ children }) => {children}, + }); expect(new URLSearchParams(window.location.search).has("bid")).toBe(false); }); it("should not preselect block when page is not loaded", () => { window.history.replaceState({}, "", "/?bid=block-1"); + const store = createTestStore([ + [presentBlocksAtom, blocks], + [selectedBlockIdsAtom, []], + [isPageLoadedAtom, false], + ]); + + renderHook(() => useBlockSelectionQuerySync(), { + wrapper: ({ children }) => {children}, + }); - const { result } = renderHook( - () => { - useBlockSelectionQuerySync(); - return useSelectedBlockIds(); - }, - { - wrapper: ({ children }) => ( - - {children} - - ), - }, - ); - - expect(result.current[0]).toEqual([]); + expect(store.get(selectedBlockIdsAtom)).toEqual([]); }); it("should update URL when block selection changes", () => { - const { result } = renderHook( - () => { - useBlockSelectionQuerySync(); - return useSelectedBlockIds(); - }, - { - wrapper: ({ children }) => ( - - {children} - - ), - }, - ); + const store = createTestStore([ + [presentBlocksAtom, blocks], + [selectedBlockIdsAtom, []], + [isPageLoadedAtom, true], + ]); + + renderHook(() => useBlockSelectionQuerySync(), { + wrapper: ({ children }) => {children}, + }); act(() => { - result.current[1](["block-3"]); + store.set(selectedBlockIdsAtom, ["block-3"]); }); expect(new URLSearchParams(window.location.search).get("bid")).toBe("block-3"); }); - it("should remove block param from URL when selection is cleared", () => { + it("should remove block param from URL when selection is cleared", async () => { window.history.replaceState({}, "", "/?bid=block-1"); - - const { result } = renderHook( - () => { - useBlockSelectionQuerySync(); - return useSelectedBlockIds(); - }, - { - wrapper: ({ children }) => ( - - {children} - - ), - }, - ); + const store = createTestStore([ + [presentBlocksAtom, blocks], + [selectedBlockIdsAtom, []], + [isPageLoadedAtom, true], + ]); + + renderHook(() => useBlockSelectionQuerySync(), { + wrapper: ({ children }) => {children}, + }); // First it restores block-1 from URL - expect(result.current[0]).toEqual(["block-1"]); + await waitFor(() => { + expect(store.get(selectedBlockIdsAtom)).toEqual(["block-1"]); + }); // Now clear selection act(() => { - result.current[1]([]); + store.set(selectedBlockIdsAtom, []); }); expect(new URLSearchParams(window.location.search).has("bid")).toBe(false);