diff --git a/packages/mini-markdown-editor/src/hooks/__test__/use-expose-handle.test.ts b/packages/mini-markdown-editor/src/hooks/__test__/use-expose-handle.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..db7802e15dd713c518f5b83896fac2f1794d2d74 --- /dev/null +++ b/packages/mini-markdown-editor/src/hooks/__test__/use-expose-handle.test.ts @@ -0,0 +1,122 @@ +import { renderHook } from "@testing-library/react"; +import { EditorView } from "@codemirror/view"; +import { useEditorContentStore } from "@/store/editor"; +import { useExposeHandle } from "../use-expose-handle"; +import { describe, test, expect, vi, beforeEach } from "vitest"; +import { insertContent } from "@/utils/insert-content"; +// 模拟 CodeMirror 的 EditorView 实例 +const mockEditorView = { + state: { + doc: "test content", + selection: { + ranges: [{ from: 0, to: 0 }], + main: { from: 0, to: 0 }, + }, + sliceDoc: vi.fn(), + }, + focus: vi.fn(), + dispatch: vi.fn(), +} as unknown as EditorView; + +// 模拟 Zustand 存储 +vi.mock("@/store/editor", () => ({ + useEditorContentStore: vi.fn(() => ({ + editorView: mockEditorView, + previewView: {}, + })), +})); + +// 模拟插入内容模块 +vi.mock("@/utils/insert-content", () => ({ + insertContent: { + insertContent: vi.fn(), + }, +})); + +describe("useExposeHandle Hook测试", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + test("应正确暴露所有编辑器操作方法", () => { + const mockRef = { current: null }; + renderHook(() => useExposeHandle(mockRef as any)); + + expect(mockRef.current).toMatchObject({ + setContent: expect.any(Function), + getContent: expect.any(Function), + clear: expect.any(Function), + setCursor: expect.any(Function), + getCursor: expect.any(Function), + getSelection: expect.any(Function), + focus: expect.any(Function), + getEditorInstance: expect.any(Function), + getPreviewInstance: expect.any(Function), + }); + }); + + test("setContent 应正确插入内容", () => { + const mockRef: { current: { setContent: (s: string) => void } | null } = { current: null }; + renderHook(() => useExposeHandle(mockRef as any)); + + mockRef.current?.setContent("new content"); + expect(mockEditorView.focus).toHaveBeenCalled(); + expect(vi.mocked(insertContent.insertContent)).toHaveBeenCalledWith( + "new content", + { anchor: 11, head: 11 }, // "new content" 长度 + ); + }); + + test("getContent 应返回编辑器内容", () => { + const mockRef: { current: { getContent: () => string } | null } = { current: null }; + renderHook(() => useExposeHandle(mockRef as any)); + + const content = mockRef.current?.getContent(); + expect(content).toBe("test content"); + }); + + test("clear 应清空编辑器内容", () => { + const mockRef: { current: { clear: () => void } | null } = { current: null }; + renderHook(() => useExposeHandle(mockRef as any)); + + mockRef.current?.clear(); + expect(mockEditorView.dispatch).toHaveBeenCalledWith({ + changes: { from: 0, to: 12, insert: "" }, // "test content" 长度 + }); + }); + + test("setCursor 应正确处理错误输入", () => { + const mockRef: { current: { setCursor: (num1: number, num2: number) => void } | null } = { + current: null, + }; + renderHook(() => useExposeHandle(mockRef as any)); + expect(() => mockRef.current?.setCursor(5, 10)).toThrowError("start 必须比 end 大"); // start < end + }); + + test("getSelection 应返回选中内容", () => { + const mockRef: { current: { getSelection: () => string } | null } = { current: null }; + renderHook(() => useExposeHandle(mockRef as any)); + mockRef.current?.getSelection(); + expect(mockEditorView.state.sliceDoc).toHaveBeenCalledWith(0, 0); + }); + + test("当编辑器实例不存在时应安全处理", () => { + // 覆盖模拟存储返回空实例 + vi.mocked(useEditorContentStore).mockReturnValueOnce({ editorView: null } as any); + + interface MockRef { + current: { + getContent: () => void; + getCursor: () => void; + setContent: (s: string) => void; + } | null; + } + + const mockRef: MockRef = { current: null }; + renderHook(() => useExposeHandle(mockRef as any)); + + expect(mockRef.current?.getContent()).toBe(""); + expect(mockRef.current?.getCursor()).toEqual({ from: 0, to: 0 }); + expect(() => mockRef.current?.setContent("test")).not.toThrow(); + }); +}); diff --git a/packages/mini-markdown-editor/src/hooks/__test__/use-global-config.test.tsx b/packages/mini-markdown-editor/src/hooks/__test__/use-global-config.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b963f8875c7d904ef7587e0002fb866ab074d2b0 --- /dev/null +++ b/packages/mini-markdown-editor/src/hooks/__test__/use-global-config.test.tsx @@ -0,0 +1,66 @@ +import { renderHook, act, waitFor } from "@testing-library/react"; +import { describe, test, expect, vi } from "vitest"; +import { type ReactNode, createContext } from "react"; +import { useGlobalConfig } from "../use-global-config"; +import { ConfigContext } from "@/components/providers/config-provider"; + +// 模拟原始上下文提供者(需保持与源码相同的Context对象) +vi.mock("@/components/providers/config-provider", () => ({ + ConfigContext: createContext(undefined), +})); + +// 创建模拟上下文类型 +type MockConfig = { theme: "light" | "dark" }; + +// 测试用 Provider 组件 +const MockProvider = ({ children, value }: { children: ReactNode; value: MockConfig }) => ( + {children} +); + +describe("useGlobalConfig Hook测试", () => { + test("当存在 ConfigProvider 时返回有效上下文", () => { + const expectedConfig: MockConfig = { theme: "dark" }; + + const { result } = renderHook(() => useGlobalConfig(), { + wrapper: ({ children }) => {children}, + }); + + expect(result.current).toEqual(expectedConfig); + }); + + test("当缺少 ConfigProvider 时抛出指定错误", () => { + // 禁用 React 的错误日志避免测试输出混乱 + const consoleError = vi.spyOn(console, "error").mockImplementation(() => null); + + expect(() => { + renderHook(() => useGlobalConfig()); // 不包裹 Provider + }).toThrowError("GlobalConfig init error"); + + consoleError.mockRestore(); + }); + + test("应响应上下文更新", () => { + const initialConfig: MockConfig = { theme: "light" }; + const updatedConfig: MockConfig = { theme: "dark" }; + + const { result, rerender } = renderHook(() => useGlobalConfig(), { + wrapper: ({ children }) => {children}, + }); + + // 初始值验证 + expect(result.current).toEqual(initialConfig); + + // 更新 Provider 值 + act(() => { + rerender({ + wrapper: ({ children }: { children: ReactNode }) => ( + {children} + ), + }); + }); + waitFor(() => { + // 验证更新后的值 + expect(result.current).toEqual(updatedConfig); + }); + }); +}); diff --git a/packages/mini-markdown-editor/src/hooks/__test__/use-init-sync-scroll-status.test.ts b/packages/mini-markdown-editor/src/hooks/__test__/use-init-sync-scroll-status.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..daffb1d634d9117fb0f16741b45028c1253cf70c --- /dev/null +++ b/packages/mini-markdown-editor/src/hooks/__test__/use-init-sync-scroll-status.test.ts @@ -0,0 +1,73 @@ +import { renderHook, act } from "@testing-library/react"; +import { describe, test, expect, vi, beforeEach } from "vitest"; +import { useInitSyncScrollStatus } from "../use-init-sync-scroll-status"; +import { SYNC_SCROLL_STATUS } from "@/common"; + +// 模拟 localStorage +const mockLocalStorage = (() => { + let store: Record = {}; + return { + getItem: vi.fn((key: string) => store[key] || null), + setItem: vi.fn((key: string, value: string) => (store[key] = value)), + clear: vi.fn(() => (store = {})), + }; +})(); + +// 模拟安全存储模块 +vi.mock("@/utils/storage", () => ({ + safeLocalStorage: () => mockLocalStorage, +})); + +describe("useInitSyncScrollStatus Hook测试", () => { + beforeEach(() => { + mockLocalStorage.clear(); + vi.clearAllMocks(); + }); + + test("应正确初始化默认状态(无本地存储)", () => { + const { result } = renderHook(() => useInitSyncScrollStatus()); + + expect(result.current.isSyncScroll).toBe(true); + expect(mockLocalStorage.setItem).toHaveBeenCalledWith(SYNC_SCROLL_STATUS, "true"); + }); + + test("应正确读取本地存储中的 true 值", () => { + mockLocalStorage.setItem(SYNC_SCROLL_STATUS, "true"); + + const { result } = renderHook(() => useInitSyncScrollStatus()); + expect(result.current.isSyncScroll).toBe(true); + }); + + test("应正确读取本地存储中的 false 值", () => { + mockLocalStorage.setItem(SYNC_SCROLL_STATUS, "false"); + + const { result } = renderHook(() => useInitSyncScrollStatus()); + expect(result.current.isSyncScroll).toBe(false); + }); + + test("更新状态应同时修改本地存储和状态", () => { + const { result } = renderHook(() => useInitSyncScrollStatus()); + + act(() => { + result.current.updateSyncScrollStatus(false); + }); + + expect(result.current.isSyncScroll).toBe(false); + expect(mockLocalStorage.setItem).toHaveBeenCalledWith(SYNC_SCROLL_STATUS, "false"); + }); + + test("初始化时应只执行一次存储设置", () => { + renderHook(() => useInitSyncScrollStatus()); + renderHook(() => useInitSyncScrollStatus()); // 二次渲染 + + // 验证 setItem 只调用一次(初始设置) + expect(mockLocalStorage.setItem).toHaveBeenCalledTimes(1); + }); + + test("应正确处理非法存储值", () => { + mockLocalStorage.setItem(SYNC_SCROLL_STATUS, "invalid"); + + const { result } = renderHook(() => useInitSyncScrollStatus()); + expect(result.current.isSyncScroll).toBe(true); // 非 false 字符串视为 true + }); +}); diff --git a/packages/mini-markdown-editor/src/hooks/__test__/use-persist-editor-content.test.ts b/packages/mini-markdown-editor/src/hooks/__test__/use-persist-editor-content.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..6ab22637d8ba1df9c46185ca9cb31db664276c95 --- /dev/null +++ b/packages/mini-markdown-editor/src/hooks/__test__/use-persist-editor-content.test.ts @@ -0,0 +1,121 @@ +import { renderHook, act, waitFor } from "@testing-library/react"; +import { vi, describe, test, expect, beforeEach, afterEach } from "vitest"; +import { usePersistEditorContent } from "../use-persist-editor-content"; +import { EDITOR_CONTENT_KEY } from "@/common"; +import { useGlobalConfig } from "../use-global-config"; +// 模拟依赖项 +vi.mock("@/utils/storage", () => ({ + safeLocalStorage: () => ({ + getItem: vi.fn(), + setItem: vi.fn(), + removeItem: vi.fn(), + }), +})); + +vi.mock("../use-global-config", () => ({ + useGlobalConfig: vi.fn(() => ({ + local: true, + value: "global-value", + })), +})); + +vi.mock("ahooks", () => ({ + useDebounceFn: (fn: any, options: any) => ({ + run: vi.fn((...args) => { + setTimeout(() => fn(...args), options.wait); + }), + }), +})); + +describe("usePersistEditorContent Hook测试", () => { + const mockLocalStorage = { + getItem: vi.fn(), + setItem: vi.fn(), + removeItem: vi.fn(), + }; + + beforeEach(() => { + vi.clearAllMocks(); + vi.useFakeTimers(); + + // 重置模拟实现 + mockLocalStorage.getItem.mockImplementation(() => null); + vi.mocked(useGlobalConfig).mockImplementation(() => ({ + local: true, + value: "global-value", + })); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + test("当 local=true 时应保存到 localStorage", async () => { + const { result } = renderHook(() => usePersistEditorContent()); + + act(() => { + result.current.saveContent("test-content"); + vi.advanceTimersByTime(300); + }); + waitFor(() => { + expect(mockLocalStorage.setItem).toHaveBeenCalledWith(EDITOR_CONTENT_KEY, "test-content"); + }); + }); + + test("当 local=false 时不应保存到 localStorage", () => { + vi.mocked(useGlobalConfig).mockReturnValue({ local: false, value: "" }); + const { result } = renderHook(() => usePersistEditorContent()); + + act(() => { + result.current.saveContent("test-content"); + vi.advanceTimersByTime(300); + }); + waitFor(() => { + expect(mockLocalStorage.setItem).not.toHaveBeenCalled(); + }); + }); + + test("防抖功能应正确生效", () => { + const { result } = renderHook(() => usePersistEditorContent()); + + act(() => { + result.current.saveContent("first"); + result.current.saveContent("second"); + vi.advanceTimersByTime(299); + }); + + expect(mockLocalStorage.setItem).not.toHaveBeenCalled(); + + act(() => { + vi.advanceTimersByTime(1); // 累计 300ms + }); + waitFor(() => { + expect(mockLocalStorage.setItem).toHaveBeenCalledTimes(1); + expect(mockLocalStorage.setItem).toHaveBeenCalledWith(EDITOR_CONTENT_KEY, "second"); + }); + }); + + test("当 local=false 且存在 value 时应返回全局值", () => { + vi.mocked(useGlobalConfig).mockReturnValue({ + local: false, + value: "custom-value", + }); + + const { result } = renderHook(() => usePersistEditorContent()); + expect(result.current.getContent()).toBe("custom-value"); + }); + + test("当 local=true 时应返回 localStorage 值", () => { + mockLocalStorage.getItem.mockReturnValue("stored-content"); + const { result } = renderHook(() => usePersistEditorContent()); + waitFor(() => { + expect(result.current.getContent()).toBe("stored-content"); + }); + }); + + test("当存储为空时应返回空字符串", () => { + mockLocalStorage.getItem.mockReturnValue(null); + const { result } = renderHook(() => usePersistEditorContent()); + expect(result.current.getContent()).toBe(""); + }); +}); diff --git a/packages/mini-markdown-editor/src/hooks/__test__/use-preview-theme.test.ts b/packages/mini-markdown-editor/src/hooks/__test__/use-preview-theme.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..46262bd7b631eee60a4342483c7799370c1d9ca2 --- /dev/null +++ b/packages/mini-markdown-editor/src/hooks/__test__/use-preview-theme.test.ts @@ -0,0 +1,80 @@ +import { renderHook } from "@testing-library/react"; +import { vi, describe, test, expect, beforeEach, afterEach } from "vitest"; +import { usePreviewTheme } from "../use-preview-theme"; + +// 模拟主题配置 +const mockPreviewTheme = { + light: { color: "#ffffff", bg: "#000000" }, + dark: { color: "#000000", bg: "#ffffff" }, +}; + +// 模拟主题模块 +vi.mock("@/theme/preview-theme", () => ({ + previewTheme: { + light: { color: "#ffffff", bg: "#000000" }, + dark: { color: "#000000", bg: "#ffffff" }, + }, +})); + +describe("usePreviewTheme Hook测试", () => { + let setPropertySpy: any; + + beforeEach(() => { + // 重置 DOM 操作 spy + setPropertySpy = vi.spyOn(document.body.style, "setProperty"); + document.body.style.cssText = ""; // 清空样式 + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + test("应正确应用 light 主题变量", () => { + renderHook(() => usePreviewTheme("light")); + + expect(setPropertySpy).toHaveBeenCalledWith("--md-preview-color", mockPreviewTheme.light.color); + expect(setPropertySpy).toHaveBeenCalledWith("--md-preview-bg", mockPreviewTheme.light.bg); + }); + + test("应正确应用 dark 主题变量", () => { + renderHook(() => usePreviewTheme("dark")); + + expect(setPropertySpy).toHaveBeenCalledWith("--md-preview-color", mockPreviewTheme.dark.color); + expect(setPropertySpy).toHaveBeenCalledWith("--md-preview-bg", mockPreviewTheme.dark.bg); + }); + + test("当主题切换时应更新变量", async () => { + const { rerender } = renderHook( + ({ theme }: { theme: "light" | "dark" }) => usePreviewTheme(theme), + { initialProps: { theme: "light" } }, + ); + + // 初始应用 light 主题 + expect(setPropertySpy).toHaveBeenCalledTimes(2); + + setPropertySpy.mockClear(); + + // 切换为 dark 主题 + rerender({ theme: "dark" }); + + expect(setPropertySpy).toHaveBeenCalledWith("--md-preview-color", mockPreviewTheme.dark.color); + expect(setPropertySpy).toHaveBeenCalledWith("--md-preview-bg", mockPreviewTheme.dark.bg); + }); + + test("应清理旧主题变量", () => { + const removePropertySpy = vi.spyOn(document.body.style, "removeProperty"); + + const { rerender } = renderHook( + ({ theme }: { theme: "light" | "dark" }) => usePreviewTheme(theme), + { initialProps: { theme: "light" } }, + ); + + // 切换主题前设置旧变量 + document.body.style.setProperty("--md-preview-old", "value"); + + rerender({ theme: "dark" }); + + // 验证没有清理操作(根据当前实现逻辑) + expect(removePropertySpy).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/mini-markdown-editor/src/hooks/__test__/use-save-content.test.ts b/packages/mini-markdown-editor/src/hooks/__test__/use-save-content.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..064d8b9bc9653dd9fee317977fba29ad1766e709 --- /dev/null +++ b/packages/mini-markdown-editor/src/hooks/__test__/use-save-content.test.ts @@ -0,0 +1,82 @@ +import { renderHook, act } from "@testing-library/react"; +import { vi, describe, test, expect, beforeEach, afterEach } from "vitest"; +import { useSaveContent } from "../use-save-content"; +import { EDITOR_CONTENT_KEY } from "@/common"; + +// 模拟 localStorage +const mockLocalStorage = { + setItem: vi.fn(), +}; + +// 模拟依赖项 +vi.mock("@/utils/storage", () => ({ + safeLocalStorage: () => mockLocalStorage, +})); + +describe("useSaveContent Hook测试", () => { + beforeEach(() => { + vi.useFakeTimers(); + mockLocalStorage.setItem.mockClear(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + test("应在防抖延迟后保存内容", () => { + const { result } = renderHook(() => useSaveContent()); + + act(() => { + result.current("test-content"); + vi.advanceTimersByTime(299); + }); + + expect(mockLocalStorage.setItem).not.toHaveBeenCalled(); + + act(() => { + vi.advanceTimersByTime(1); + }); + + expect(mockLocalStorage.setItem).toHaveBeenCalledWith(EDITOR_CONTENT_KEY, "test-content"); + }); + + test("多次调用应只执行最后一次保存", () => { + const { result } = renderHook(() => useSaveContent()); + + act(() => { + result.current("content-1"); + result.current("content-2"); + result.current("final-content"); + vi.advanceTimersByTime(300); + }); + + expect(mockLocalStorage.setItem).toHaveBeenCalledTimes(1); + expect(mockLocalStorage.setItem).toHaveBeenCalledWith(EDITOR_CONTENT_KEY, "final-content"); + }); + + test("应正确处理空内容", () => { + const { result } = renderHook(() => useSaveContent()); + + act(() => { + result.current(""); + vi.advanceTimersByTime(300); + }); + + expect(mockLocalStorage.setItem).toHaveBeenCalledWith(EDITOR_CONTENT_KEY, ""); + }); + + test("应使用正确的防抖时间", () => { + const { result } = renderHook(() => useSaveContent()); + + act(() => { + result.current("test-timing"); + vi.advanceTimersByTime(299); + }); + expect(mockLocalStorage.setItem).not.toHaveBeenCalled(); + + act(() => { + vi.advanceTimersByTime(1); + }); + expect(mockLocalStorage.setItem).toHaveBeenCalled(); + }); +}); diff --git a/packages/mini-markdown-editor/src/hooks/__test__/use-sync-editorview.test.ts b/packages/mini-markdown-editor/src/hooks/__test__/use-sync-editorview.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..36bc18bb79a6dfd78d2f87574fb63c5175f80805 --- /dev/null +++ b/packages/mini-markdown-editor/src/hooks/__test__/use-sync-editorview.test.ts @@ -0,0 +1,73 @@ +import { renderHook } from "@testing-library/react"; +import { vi, describe, test, expect, beforeEach } from "vitest"; +import { useSyncEditorView } from "../use-sync-editorview"; +import { useEditorContentStore } from "@/store/editor"; +import { insertContent } from "@/utils/insert-content"; + +// 模拟依赖模块 +vi.mock("@/store/editor", () => ({ + useEditorContentStore: vi.fn(), +})); + +vi.mock("@/utils/insert-content", () => ({ + insertContent: { + setEditorView: vi.fn(), + }, +})); + +describe("useSyncEditorView Hook测试", () => { + const mockEditorView = { id: "editor-1" }; + const mockSetEditorView = vi.mocked(insertContent.setEditorView); + + beforeEach(() => { + vi.clearAllMocks(); + // 默认返回有效 editorView + vi.mocked(useEditorContentStore).mockImplementation((selector: any) => + selector({ editorView: mockEditorView }), + ); + }); + + test("应在挂载时同步 editorView 实例", () => { + renderHook(() => useSyncEditorView()); + + expect(mockSetEditorView).toHaveBeenCalledTimes(1); + expect(mockSetEditorView).toHaveBeenCalledWith(mockEditorView); + }); + + test("当 editorView 变化时应重新同步", () => { + const { rerender } = renderHook(() => useSyncEditorView()); + + // 初始调用 + expect(mockSetEditorView).toHaveBeenCalledTimes(1); + + // 更新 editorView + const newEditorView = { id: "editor-2" }; + vi.mocked(useEditorContentStore).mockImplementation((selector: any) => + selector({ editorView: newEditorView }), + ); + rerender(); + + expect(mockSetEditorView).toHaveBeenCalledTimes(2); + expect(mockSetEditorView).toHaveBeenCalledWith(newEditorView); + }); + + test("当 editorView 为 null 时应安全处理", () => { + vi.mocked(useEditorContentStore).mockImplementation((selector: any) => + selector({ editorView: null }), + ); + + renderHook(() => useSyncEditorView()); + + expect(mockSetEditorView).toHaveBeenCalledWith(null); + }); + + test("应在每次渲染时同步(无依赖数组)", () => { + const { rerender } = renderHook(() => useSyncEditorView()); + + rerender(); // 强制重新渲染 + rerender(); // 再次重新渲染 + + // 预期每次渲染都会触发同步(3次初始渲染+2次重渲染) + expect(mockSetEditorView).toHaveBeenCalledTimes(3); + }); +}); diff --git a/packages/mini-markdown-editor/src/hooks/__test__/use-toolbar.test.tsx b/packages/mini-markdown-editor/src/hooks/__test__/use-toolbar.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2f89aef9d2af08416dcc39f5c789870cd4a7959e --- /dev/null +++ b/packages/mini-markdown-editor/src/hooks/__test__/use-toolbar.test.tsx @@ -0,0 +1,62 @@ +import { renderHook, waitFor } from "@testing-library/react"; +import { describe, test, expect, vi } from "vitest"; +import { useToolbar } from "../use-toolbar"; +import { type ReactNode } from "react"; +import { ToolbarContext } from "@/components/providers/toolbar-provider"; +import { ToolbarContextValues } from "@/types/toolbar"; + +// 测试用 Provider 组件 +const MockProvider = ({ + children, + value, +}: { + children: ReactNode; + value: ToolbarContextValues; +}) => {children}; + +describe("useToolbar Hook测试", () => { + test("当存在 ToolbarProvider 时返回有效上下文", () => { + const expectedValue: ToolbarContextValues = { toolbars: [{ type: "file" }] }; + + const { result } = renderHook(() => useToolbar(), { + wrapper: ({ children }) => {children}, + }); + + expect(result.current).toEqual(expectedValue); + }); + + test("当缺少 ToolbarProvider 时抛出指定错误", () => { + // 禁用 React 的错误日志避免测试输出混乱 + const consoleError = vi.spyOn(console, "error").mockImplementation(() => null); + + expect(() => { + renderHook(() => useToolbar()); // 不包裹 Provider + }).toThrowError("Toolbar init error"); + + consoleError.mockRestore(); + }); + + test("应响应上下文值的更新", () => { + const initialValue: ToolbarContextValues = { toolbars: [{ type: "file" }] }; + const updatedValue: ToolbarContextValues = { toolbars: [{ type: "file" }, { type: "edit" }] }; + + const { result, rerender } = renderHook(() => useToolbar(), { + wrapper: ({ children }) => {children}, + }); + + // 验证初始值 + expect(result.current).toEqual(initialValue); + + // 更新 Provider 值并重新渲染 + rerender({ + wrapper: ({ children }: { children: ReactNode }) => ( + {children} + ), + }); + + waitFor(() => { + // 验证更新后的值 + expect(result.current).toEqual(updatedValue); + }); + }); +}); diff --git a/packages/mini-markdown-editor/src/hooks/use-expose-handle.ts b/packages/mini-markdown-editor/src/hooks/use-expose-handle.ts index edee6c10994093db32ba1d5b285baf9a787a953c..a292c6371015080ac24e30bd8db4136323e3cbf2 100644 --- a/packages/mini-markdown-editor/src/hooks/use-expose-handle.ts +++ b/packages/mini-markdown-editor/src/hooks/use-expose-handle.ts @@ -40,8 +40,7 @@ class ExposeHandle { public setCursor(start: number, end: number) { if (!this.view) return; if (start < end) { - new Error("start 必须比 end 大"); - return; + throw new Error("start 必须比 end 大"); } // 获取光标位置 const { from, to } = this.view.state.selection.ranges[0];