From bca9c7ff77239a2b30bf87d541ca0a9df8b19cdb Mon Sep 17 00:00:00 2001 From: link <3399652842@qq.com> Date: Sat, 1 Mar 2025 02:07:12 +0800 Subject: [PATCH 1/2] =?UTF-8?q?test(mini-markdown-editor):=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E6=B5=8B=E8=AF=95=EF=BC=8C=E5=B0=BD=E5=8F=AF=E8=83=BD?= =?UTF-8?q?=E6=8F=90=E5=8D=87=E4=BA=86coverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/Editor/__test__/index.test.tsx | 212 +++++++++++++++--- .../Toolbar/__test__/CopyCodeButton.test.tsx | 1 - .../Toolbar/__test__/ToolbarItem.test.tsx | 1 - .../base/__test__/DropDownMenu.test.tsx | 15 ++ .../src/utils/__test__/handle-scroll.test.ts | 61 ++++- .../src/utils/__test__/storage.test.ts | 22 +- 6 files changed, 272 insertions(+), 40 deletions(-) diff --git a/packages/mini-markdown-editor/src/components/Editor/__test__/index.test.tsx b/packages/mini-markdown-editor/src/components/Editor/__test__/index.test.tsx index 9ea8492..3210a95 100644 --- a/packages/mini-markdown-editor/src/components/Editor/__test__/index.test.tsx +++ b/packages/mini-markdown-editor/src/components/Editor/__test__/index.test.tsx @@ -1,52 +1,192 @@ -import { describe, test, expect, vi } from "vitest"; -import { render, fireEvent } from "@testing-library/react"; -import "@testing-library/jest-dom"; -import { ConfigContext } from "../../providers/config-provider"; - +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { describe, test, expect, vi, beforeEach } from "vitest"; import Editor from "../index"; +import { useEditorContentStore } from "@/store/editor"; +import { usePersistEditorContent } from "@/hooks/use-persist-editor-content"; +import { toolbarConfig } from "@/config/toolbar"; +import { handleEditorScroll } from "@/utils/handle-scroll"; +import { createEditorExtensions } from "@/extensions/codemirror"; +import { ChangeEvent } from "react"; +import { useGlobalConfig } from "@/hooks/use-global-config"; +// 模拟核心依赖项 +vi.mock("@/hooks/use-global-config", () => { + return { + useGlobalConfig: vi.fn(), + }; +}); + +vi.mock("@uiw/react-codemirror", () => ({ + default: ({ extensions, onChange, onCreateEditor, basicSetup, ...props }: any) => { + const handleInput = (e: ChangeEvent) => { + onChange(e.target.textContent, { view: null }); + }; + const handleScroll = () => { + extensions.scroll(); + }; + onCreateEditor(document.createElement("div")); + return ( + + ); + }, +})); + +vi.mock("@/store/editor", () => ({ + useEditorContentStore: vi.fn(), +})); + +vi.mock("@/hooks/use-persist-editor-content", () => { + const getContent = vi.fn(() => "initial content"); + const saveContent = vi.fn(); + return { + usePersistEditorContent: vi.fn(() => ({ + saveContent, + getContent, + })), + }; +}); + +vi.mock("@/utils/handle-scroll", () => ({ + handleEditorScroll: vi.fn(), +})); + +vi.mock("@/config/toolbar", () => ({ + toolbarConfig: { + getAllToolbars: vi.fn(() => []), + }, +})); + +// 模拟 CodeMirror 扩展 +vi.mock("@/extensions/codemirror", () => ({ + createEditorExtensions: vi.fn(({ eventExt }: any) => ({ scroll: eventExt.scroll })), +})); + +//模拟事件 +vi.mock("@uiw/codemirror-extensions-events", () => ({ + scroll: (arg: any) => arg, +})); describe("Editor 组件测试", () => { - test("测试组件基本渲染", async () => { - const { container } = render(); - expect(container.querySelector(".markdown-editor-content")).toBeInTheDocument(); + const mockSetContent = vi.fn(); + const mockSetEditorView = vi.fn(); + const mockUseEditorContentStore = vi.mocked(useEditorContentStore); + const mockToolbarConfig = vi.mocked(toolbarConfig); + const onChange = vi.fn(), + onDragUpload = vi.fn(), + onPatseUpload = vi.fn(); + const editorContentStoreReturnValue = { + content: "test content", + setContent: mockSetContent, + scrollWrapper: "editor", + setScrollWrapper: vi.fn(), + setEditorView: mockSetEditorView, + previewView: document.createElement("div"), + editorView: { + dispatch: vi.fn(), + view: document.createElement("div"), + } as any, + }; + const globalConfigRuturnValue = { + theme: "light" as const, + lineNumbers: true, + enableShortcuts: true, + onChange, + onDragUpload, + onPatseUpload, + }; + beforeEach(() => { + vi.clearAllMocks(); + mockUseEditorContentStore.mockReturnValue(editorContentStoreReturnValue); + vi.mocked(useGlobalConfig).mockReturnValue(globalConfigRuturnValue); }); - test("测试滚动同步功能", async () => { - const { container } = render(); - const editorElement = container.querySelector(".markdown-editor-content"); - expect(editorElement).toBeInTheDocument(); + test("应正确初始化编辑器", () => { + render(); + + // 验证 CodeMirror 渲染 + expect(screen.getByTestId("codemirror-mock")).toBeInTheDocument(); + expect(screen.getByText("test content")).toBeInTheDocument(); - // 触发鼠标进入事件 - if (editorElement) { - fireEvent.mouseEnter(editorElement); - } + // 验证持久化内容获取 + expect(usePersistEditorContent().getContent).toHaveBeenCalled(); }); - test("测试编辑器内容变更事件", async () => { - const mockOnChange = vi.fn(); - const { container } = render( - - - , - ); + test("内容变更应触发保存和回调", async () => { + const mockSaveContent = vi.fn(); + vi.mocked(usePersistEditorContent).mockReturnValueOnce({ + saveContent: mockSaveContent, + getContent: vi.fn(), + }); + + render(); - expect(container.querySelector(".markdown-editor-content")).toBeInTheDocument(); + // 模拟内容变更 + const newContent = "new content"; + fireEvent.input(screen.getByTestId("codemirror-mock"), { target: { textContent: newContent } }); + await waitFor(() => { + expect(mockSaveContent).toHaveBeenCalledWith(newContent); + expect(mockSaveContent).toHaveBeenCalled(); + }); }); - test("测试主题切换功能", async () => { - const { container, rerender } = render( - - - , - ); + test("应处理滚动同步逻辑", async () => { + render(); + + // 模拟滚动事件 + const scrollEvent = new Event("scroll"); + screen.getByTestId("codemirror-mock").dispatchEvent(scrollEvent); + + await waitFor(() => { + expect(handleEditorScroll).toHaveBeenCalled(); + }); + }); - // 重新渲染暗色主题 - rerender( - - - , + test("应响应主题和行号配置变化", () => { + vi.mocked(useGlobalConfig).mockReturnValue({ + ...globalConfigRuturnValue, + theme: "dark", + lineNumbers: true, + }); + const { rerender } = render(); + + // 验证初始配置 + expect(screen.getByTestId("codemirror-mock")).toHaveAttribute("theme", "dark"); + vi.mocked(useGlobalConfig).mockReturnValue({ + ...globalConfigRuturnValue, + theme: "light", + lineNumbers: false, + }); + // 更新配置重新渲染 + rerender(); + expect(screen.getByTestId("codemirror-mock")).toHaveAttribute("theme", "light"); + }); + + test("应初始化编辑器扩展", () => { + mockToolbarConfig.getAllToolbars.mockReturnValueOnce([{ type: "bold" }]); + render(); + expect(createEditorExtensions).toHaveBeenCalledWith( + expect.objectContaining({ + toolbars: [{ type: "bold" }], + enableShortcuts: true, + }), ); + }); + + test("应处理空内容场景", () => { + mockUseEditorContentStore.mockReturnValueOnce({ + ...editorContentStoreReturnValue, + content: "", + setContent: vi.fn(), + }); - expect(container.querySelector(".markdown-editor-content")).toBeInTheDocument(); + render(); + expect(screen.getByTestId("codemirror-mock")).toBeEmptyDOMElement(); }); }); diff --git a/packages/mini-markdown-editor/src/components/Toolbar/__test__/CopyCodeButton.test.tsx b/packages/mini-markdown-editor/src/components/Toolbar/__test__/CopyCodeButton.test.tsx index 9d2e12e..b263a6a 100644 --- a/packages/mini-markdown-editor/src/components/Toolbar/__test__/CopyCodeButton.test.tsx +++ b/packages/mini-markdown-editor/src/components/Toolbar/__test__/CopyCodeButton.test.tsx @@ -51,7 +51,6 @@ describe("CopyCodeButton 组件测试", () => { test("开发环境下根据语言显示正确文本", () => { // 测试英文环境 render(); - screen.debug(); expect(screen.getByText("Copy Test Code")).toBeInTheDocument(); // 测试中文环境 diff --git a/packages/mini-markdown-editor/src/components/Toolbar/__test__/ToolbarItem.test.tsx b/packages/mini-markdown-editor/src/components/Toolbar/__test__/ToolbarItem.test.tsx index 1f6b698..5c37a04 100644 --- a/packages/mini-markdown-editor/src/components/Toolbar/__test__/ToolbarItem.test.tsx +++ b/packages/mini-markdown-editor/src/components/Toolbar/__test__/ToolbarItem.test.tsx @@ -89,7 +89,6 @@ describe("ToolbarItem 组件测试", () => { test("应正确应用类名", () => { const { container } = render(); - screen.debug(); expect(container.firstChild).toHaveClass( `${CLASS_PREFIX}-toolbar-item ${CLASS_PREFIX}-toolbar-item-${baseProps.type}`, ); diff --git a/packages/mini-markdown-editor/src/components/base/__test__/DropDownMenu.test.tsx b/packages/mini-markdown-editor/src/components/base/__test__/DropDownMenu.test.tsx index 0f27f60..f3b1078 100644 --- a/packages/mini-markdown-editor/src/components/base/__test__/DropDownMenu.test.tsx +++ b/packages/mini-markdown-editor/src/components/base/__test__/DropDownMenu.test.tsx @@ -61,6 +61,7 @@ describe("DropDownMenu 组件测试", () => { }); testList.forEach(async (item) => { + fireEvent.mouseOver(button); const menuItem = screen.getByText(item.title!); fireEvent.click(menuItem); @@ -71,4 +72,18 @@ describe("DropDownMenu 组件测试", () => { }); }); }); + it("正确渲染自定义菜单项", () => { + const custom = () =>
custom
; + render( + + + , + ); + const button = screen.getByRole("button", { name: "heading" }); + fireEvent.mouseOver(button); + //同样需要等待 + waitFor(() => { + expect(screen.getByText("custom")).toBeInTheDocument(); + }); + }); }); diff --git a/packages/mini-markdown-editor/src/utils/__test__/handle-scroll.test.ts b/packages/mini-markdown-editor/src/utils/__test__/handle-scroll.test.ts index b551cac..95bda73 100644 --- a/packages/mini-markdown-editor/src/utils/__test__/handle-scroll.test.ts +++ b/packages/mini-markdown-editor/src/utils/__test__/handle-scroll.test.ts @@ -1,11 +1,12 @@ import { describe, test, expect, vi, beforeEach } from "vitest"; +import type { EditorView } from "@uiw/react-codemirror"; import { handleEditorScroll, handlePreviewScroll, handleScrollTop, scrollSynchronizer, } from "../handle-scroll"; -import type { EditorView } from "@codemirror/view"; +import { waitFor } from "@testing-library/react"; // 模拟 DOM 环境 const mockPreviewView = document.createElement("div"); @@ -63,4 +64,62 @@ describe("handle-scroll Utils测试", () => { expect(spy).toHaveBeenCalledWith(mockEditorView, mockPreviewView); }); }); + + // 测试 ScrollSynchronizer 类的私有方法 + describe("ScrollSynchronizer 私有方法测试", () => { + test("computeHeightMapping 应正确计算高度映射", () => { + const instance = scrollSynchronizer; + const spy = vi.spyOn(instance as any, "clearHeightMappings"); + + instance["computeHeightMapping"]({ + editorView: mockEditorView, + previewView: mockPreviewView, + }); + + expect(spy).toHaveBeenCalled(); + }); + + test("synchronizeScroll 应正确同步滚动", () => { + const instance = scrollSynchronizer; + const spy = vi.spyOn(instance as any, "performProportionalScroll"); + + instance["synchronizeScroll"]("editor", { + editorView: mockEditorView, + previewView: mockPreviewView, + }); + waitFor(() => { + expect(spy).toHaveBeenCalled(); + }); + }); + + test("scrollToTop 应正确滚动到顶部", () => { + const instance = scrollSynchronizer; + + instance["scrollToTop"](mockEditorView, mockPreviewView); + + expect(mockEditorView.scrollDOM.scrollTo).toHaveBeenCalledWith({ + top: 0, + behavior: "smooth", + }); + expect(mockPreviewView.scrollTo).toHaveBeenCalledWith({ + top: 0, + behavior: "smooth", + }); + }); + + test("scrollToBottom 应正确滚动到底部", () => { + const instance = scrollSynchronizer; + const targetElement = document.createElement("div"); + const content = document.createElement("div"); + targetElement.appendChild(content); + targetElement.style.height = "100px"; + content.style.height = "200px"; + targetElement.scrollTop = 50; + instance["scrollToBottom"](targetElement); + + waitFor(() => { + expect(targetElement.scrollTop).toBe(200 - 100); + }); + }); + }); }); diff --git a/packages/mini-markdown-editor/src/utils/__test__/storage.test.ts b/packages/mini-markdown-editor/src/utils/__test__/storage.test.ts index 3833a93..cbcd4d9 100644 --- a/packages/mini-markdown-editor/src/utils/__test__/storage.test.ts +++ b/packages/mini-markdown-editor/src/utils/__test__/storage.test.ts @@ -22,9 +22,10 @@ const mockLocalStorage = (() => { }; })(); -describe("safeLocalStorage Utils测试", () => { +describe("safeLocalStorage", () => { let originalLocalStorage: Storage; const consoleError = vi.spyOn(console, "error"); + const consoleWarn = vi.spyOn(console, "warn"); beforeEach(() => { // 重置模拟 localStorage @@ -75,6 +76,18 @@ describe("safeLocalStorage Utils测试", () => { }); describe("异常处理测试", () => { + test("不可用的 localStorage", () => { + // 模拟 localStorage 不可用 + (window as any).localStorage = null; + + const storage = safeLocalStorage(); + storage.setItem("test", "value"); + expect(storage.getItem("test")).toBeNull(); + storage.removeItem("test"); + storage.clear(); + expect(consoleWarn).toHaveBeenCalledTimes(4); + }); + test("损坏的 JSON 数据", () => { const storage = safeLocalStorage(); mockLocalStorage.setItem("badData", "{invalid json"); @@ -104,5 +117,12 @@ describe("safeLocalStorage Utils测试", () => { const storage = safeLocalStorage(); expect(storage.getItem("nonExistent")).toBeNull(); }); + + test("存储特殊类型", () => { + const storage = safeLocalStorage(); + const date = new Date(); + storage.setItem("date", date); + expect(storage.getItem("date")).toBe(date.toISOString()); + }); }); }); -- Gitee From fa7c4151eed36f23e48293e5ee3d2f9eb23af419 Mon Sep 17 00:00:00 2001 From: link <3399652842@qq.com> Date: Sat, 1 Mar 2025 04:19:08 +0800 Subject: [PATCH 2/2] =?UTF-8?q?test(mini-markdown-editor):=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/hooks/__test__/use-copy-code.test.tsx | 204 +++++++++--------- .../src/utils/__test__/handle-scroll.test.ts | 203 ++++++++++------- .../src/utils/__test__/storage.test.ts | 2 +- 3 files changed, 226 insertions(+), 183 deletions(-) diff --git a/packages/mini-markdown-editor/src/hooks/__test__/use-copy-code.test.tsx b/packages/mini-markdown-editor/src/hooks/__test__/use-copy-code.test.tsx index 6d90966..7929bec 100644 --- a/packages/mini-markdown-editor/src/hooks/__test__/use-copy-code.test.tsx +++ b/packages/mini-markdown-editor/src/hooks/__test__/use-copy-code.test.tsx @@ -1,10 +1,9 @@ -import { render, act, waitFor } from "@testing-library/react"; -import { describe, test, expect, vi, beforeEach, afterEach } from "vitest"; -import { useRef } from "react"; -import { createRoot } from "react-dom/client"; +import { renderHook, waitFor } from "@testing-library/react"; +import { describe, test, expect, vi, beforeEach } from "vitest"; import { useCopyCode } from "../use-copy-code"; +import { createRoot } from "react-dom/client"; -// Mock ReactDOM.createRoot 和定时器 +// 模拟 ReactDOM.createRoot vi.mock("react-dom/client", () => ({ createRoot: vi.fn(() => ({ render: vi.fn(), @@ -12,140 +11,143 @@ vi.mock("react-dom/client", () => ({ })), })); -// Mock requestAnimationFrame -vi.stubGlobal("requestAnimationFrame", (fn: FrameRequestCallback) => setTimeout(fn, 0)); - -// 测试组件容器 -const TestComponent = ({ node }: { node: string }) => { - const previewRef = useRef(null); - useCopyCode({ previewRef, node }); - return
; -}; +// 模拟 CopyButton 组件 +vi.mock("@/components/Preview/CopyCodeButton", () => ({ + CopyButton: () =>
, +})); describe("useCopyCode Hook测试", () => { + const mockPreviewContainer = document.createElement("div"); + const mockCodeBlock = ` +
+
+ console.log('test') +
+ `; + const mockRef = { current: mockPreviewContainer }; + const testContainer = document.createElement("div"); beforeEach(() => { - vi.useFakeTimers(); - // 重置DOM - document.body.innerHTML = ""; vi.clearAllMocks(); - }); - afterEach(() => { - vi.clearAllTimers(); + mockPreviewContainer.innerHTML = mockCodeBlock; + document.body.appendChild(mockPreviewContainer); }); - test("应在挂载时添加复制按钮", async () => { - // 准备测试DOM结构 - const preview = document.createElement("div"); - preview.innerHTML = ` -
-
- console.log('test') -
- `; - document.body.appendChild(preview); - - const { unmount } = render(, { - container: preview, - }); + test("应正确添加复制按钮", async () => { + const { unmount } = renderHook(() => + useCopyCode({ + previewRef: mockRef, + node: "test-node", + }), + ); // 等待异步操作完成 - await act(async () => { - vi.runAllTimers(); - }); + await new Promise((resolve) => setTimeout(resolve, 0)); - // 验证按钮容器已添加 - const buttons = preview.querySelectorAll(".copy-code-button-wrapper"); + const buttons = mockPreviewContainer.querySelectorAll('[data-testid="copy-button"]'); waitFor(() => { expect(buttons.length).toBe(1); - // 验证createRoot调用 - expect(createRoot).toHaveBeenCalledTimes(1); + expect(createRoot).toHaveBeenCalled(); }); - unmount(); + unmount(); // 触发清理 }); - test("应在node变化时清理并重新创建按钮", async () => { - const preview = document.createElement("div"); - preview.innerHTML = ` -
-
- console.log('updated') -
- `; - const { rerender } = render(, { - container: preview, + test("卸载时应清理所有按钮", async () => { + const { unmount } = renderHook(() => + useCopyCode({ + previewRef: mockRef, + node: "test-node", + }), + ); + + await new Promise((resolve) => setTimeout(resolve, 0)); + unmount(); + // 验证清理函数被调用 + waitFor(() => { + expect(createRoot(testContainer).unmount).toHaveBeenCalled(); + const buttons = mockPreviewContainer.querySelectorAll('[data-testid="copy-button"]'); + expect(buttons.length).toBe(0); }); + }); - // 初始渲染 - await act(async () => { - vi.runAllTimers(); + test("依赖项变化时应重新创建按钮", async () => { + const { rerender, unmount } = renderHook((props) => useCopyCode(props), { + initialProps: { + previewRef: mockRef, + node: "node-1", + }, }); - rerender(); + await new Promise((resolve) => setTimeout(resolve, 0)); + const initialRootInstance = createRoot(testContainer); - await act(async () => { - vi.runAllTimers(); + // 更新依赖项 + rerender({ + previewRef: mockRef, + node: "node-2", }); - // 验证清理函数被调用 - const mockRoot = createRoot({} as HTMLElement); + + await new Promise((resolve) => setTimeout(resolve, 0)); waitFor(() => { - expect(mockRoot.unmount).toHaveBeenCalled(); expect(createRoot).toHaveBeenCalledTimes(2); + expect(initialRootInstance.unmount).toHaveBeenCalled(); }); - }); - test("应在卸载时执行清理", async () => { - const preview = document.createElement("div"); - const { unmount } = render(, { - container: preview, - }); + unmount(); + }); - await act(async () => { - vi.runAllTimers(); - }); + test("应处理重复添加按钮的情况", async () => { + // 添加重复按钮结构 + mockPreviewContainer.innerHTML = mockCodeBlock + mockCodeBlock; - // 执行卸载 - unmount(); + const { unmount } = renderHook(() => + useCopyCode({ + previewRef: mockRef, + node: "test-node", + }), + ); - // 验证清理函数被调用 - const mockRoot = createRoot({} as HTMLElement); + await new Promise((resolve) => setTimeout(resolve, 0)); + const buttons = mockPreviewContainer.querySelectorAll('[data-testid="copy-button"]'); waitFor(() => { - expect(mockRoot.unmount).toHaveBeenCalled(); + expect(buttons.length).toBe(2); }); + + unmount(); }); - test("应处理无匹配元素的情况", async () => { - const preview = document.createElement("div"); - preview.innerHTML = `
`; - render(, { - container: preview, - }); + test("应处理无效代码块的情况", async () => { + const consoleSpy = vi.spyOn(console, "error"); + mockPreviewContainer.innerHTML = '
'; - await act(async () => { - vi.runAllTimers(); - }); + renderHook(() => + useCopyCode({ + previewRef: mockRef, + node: "test-node", + }), + ); - expect(createRoot).not.toHaveBeenCalled(); + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(consoleSpy).not.toHaveBeenCalled(); }); - test("应跳过已存在按钮的元素", async () => { - const preview = document.createElement("div"); - preview.innerHTML = ` -
-
-
-
- existing button -
- `; - render(, { - container: preview, + test("应捕获并记录错误", async () => { + const consoleSpy = vi.spyOn(console, "error"); + const mockError = new Error("Render error"); + vi.mocked(createRoot).mockImplementationOnce(() => { + throw mockError; }); - await act(async () => { - vi.runAllTimers(); - }); + renderHook(() => + useCopyCode({ + previewRef: mockRef, + node: "test-node", + }), + ); - expect(createRoot).not.toHaveBeenCalled(); + await new Promise((resolve) => setTimeout(resolve, 0)); + waitFor(() => { + expect(consoleSpy).toHaveBeenCalledWith("Copy Error:", mockError); + }); }); }); diff --git a/packages/mini-markdown-editor/src/utils/__test__/handle-scroll.test.ts b/packages/mini-markdown-editor/src/utils/__test__/handle-scroll.test.ts index 95bda73..c59f1cc 100644 --- a/packages/mini-markdown-editor/src/utils/__test__/handle-scroll.test.ts +++ b/packages/mini-markdown-editor/src/utils/__test__/handle-scroll.test.ts @@ -1,125 +1,166 @@ -import { describe, test, expect, vi, beforeEach } from "vitest"; -import type { EditorView } from "@uiw/react-codemirror"; +import { describe, test, expect, vi, beforeEach, beforeAll, afterAll } from "vitest"; import { + scrollSynchronizer, handleEditorScroll, handlePreviewScroll, handleScrollTop, - scrollSynchronizer, } from "../handle-scroll"; +import { EditorView } from "@codemirror/view"; import { waitFor } from "@testing-library/react"; // 模拟 DOM 环境 const mockPreviewView = document.createElement("div"); -mockPreviewView.scrollTo = vi.fn(); +mockPreviewView.style.height = "300px"; +// 添加测试数据行标记 +for (let i = 1; i <= 3; i++) { + const vnode = document.createElement("div") as any; + vnode.setAttribute("data-line", i.toString()); + vnode.style.height = "50px"; + //jsdom不支持offsetTop故手动赋值 + Object.defineProperty(vnode, "offsetTop", { + value: (i - 1) * 50, + writable: true, + }); + mockPreviewView.appendChild(vnode); +} + const mockEditorView = { scrollDOM: document.createElement("div"), - state: { doc: { lines: 100 } }, + state: { + doc: { + lines: 100, + line: (num: number) => ({ from: num * 10 }), + }, + }, + lineBlockAt: (pos: number) => ({ top: pos }), + dispatch: vi.fn(), } as unknown as EditorView; -mockEditorView.scrollDOM.scrollTo = vi.fn(); describe("handle-scroll Utils测试", () => { beforeEach(() => { - // 重置所有模拟调用 - vi.restoreAllMocks(); + // 重置实例状态 + + vi.clearAllMocks(); }); - // 测试 handleEditorScroll - describe("handleEditorScroll", () => { - test("正常调用时应触发同步逻辑", () => { - const spy = vi.spyOn(scrollSynchronizer, "handleEditorScroll"); + describe("核心方法", () => { + test("应正确计算高度映射", () => { + scrollSynchronizer.handleEditorScroll(mockEditorView, mockPreviewView); + expect(scrollSynchronizer["editorElementList"]).toEqual([10, 20, 30]); + expect(scrollSynchronizer["previewElementList"]).toEqual([0, 50, 100]); + }); - handleEditorScroll({ - editorView: mockEditorView, - previewView: mockPreviewView, - }); + test("应处理无效行号", () => { + const invalidNode = document.createElement("div"); + invalidNode.setAttribute("data-line", "invalid"); + mockPreviewView.appendChild(invalidNode); - expect(spy).toHaveBeenCalledWith(mockEditorView, mockPreviewView); + scrollSynchronizer.handleEditorScroll(mockEditorView, mockPreviewView); + expect(scrollSynchronizer["editorElementList"].length).toBe(3); }); }); - // 测试 handlePreviewScroll - describe("handlePreviewScroll", () => { - test("正常调用时应触发同步逻辑", () => { - const spy = vi.spyOn(scrollSynchronizer, "handlePreviewScroll"); - - handlePreviewScroll({ - editorView: mockEditorView, - previewView: mockPreviewView, - }); + describe("滚动同步", () => { + test("顶部边界处理", () => { + mockEditorView.scrollDOM.scrollTop = 0; + scrollSynchronizer.handleEditorScroll(mockEditorView, mockPreviewView); - expect(spy).toHaveBeenCalledWith(mockPreviewView, mockEditorView); + expect(mockPreviewView.scrollTop).toBe(0); }); - }); - - // 测试 handleScrollTop - describe("handleScrollTop", () => { - test("正常调用时应触发置顶逻辑", () => { - const spy = vi.spyOn(scrollSynchronizer, "handleScrollTop"); - handleScrollTop({ - editorView: mockEditorView, - previewView: mockPreviewView, + test("底部边界处理", () => { + mockEditorView.scrollDOM.scrollTop = 9999; + scrollSynchronizer.handleEditorScroll(mockEditorView, mockPreviewView); + waitFor(() => { + // 验证触发动画逻辑 + expect(mockPreviewView.scrollTop).toBeGreaterThan(100); }); - - expect(spy).toHaveBeenCalledWith(mockEditorView, mockPreviewView); }); - }); - // 测试 ScrollSynchronizer 类的私有方法 - describe("ScrollSynchronizer 私有方法测试", () => { - test("computeHeightMapping 应正确计算高度映射", () => { - const instance = scrollSynchronizer; - const spy = vi.spyOn(instance as any, "clearHeightMappings"); - - instance["computeHeightMapping"]({ - editorView: mockEditorView, - previewView: mockPreviewView, + test("中间位置比例滚动", () => { + // 模拟编辑器滚动到中间位置 + mockEditorView.scrollDOM.scrollTop = 15; + scrollSynchronizer.handleEditorScroll(mockEditorView, mockPreviewView); + waitFor(() => { + // 预期预览区域滚动到 25px (0-50 对应 0-50px) + expect(mockPreviewView.scrollTop).toBe(25); }); - - expect(spy).toHaveBeenCalled(); }); + }); - test("synchronizeScroll 应正确同步滚动", () => { - const instance = scrollSynchronizer; - const spy = vi.spyOn(instance as any, "performProportionalScroll"); + describe("公共接口", () => { + test("handleEditorScroll 应触发同步", () => { + handleEditorScroll({ editorView: mockEditorView, previewView: mockPreviewView }); + expect(mockPreviewView.scrollTop).toBeDefined(); + }); - instance["synchronizeScroll"]("editor", { - editorView: mockEditorView, - previewView: mockPreviewView, - }); + test("handlePreviewScroll 应反向同步", () => { + mockPreviewView.scrollTop = 50; + handlePreviewScroll({ editorView: mockEditorView, previewView: mockPreviewView }); waitFor(() => { - expect(spy).toHaveBeenCalled(); + expect(mockEditorView.scrollDOM.scrollTop).toBe(10); }); }); - test("scrollToTop 应正确滚动到顶部", () => { - const instance = scrollSynchronizer; + test("handleScrollTop 应重置滚动位置", () => { + const mockScroll = vi.fn(); + mockEditorView.scrollDOM.scrollTo = mockScroll; + mockPreviewView.scrollTo = mockScroll; - instance["scrollToTop"](mockEditorView, mockPreviewView); + handleScrollTop({ editorView: mockEditorView, previewView: mockPreviewView }); + expect(mockScroll).toHaveBeenCalledTimes(2); + }); + }); - expect(mockEditorView.scrollDOM.scrollTo).toHaveBeenCalledWith({ - top: 0, - behavior: "smooth", - }); - expect(mockPreviewView.scrollTo).toHaveBeenCalledWith({ - top: 0, - behavior: "smooth", - }); + describe("边界条件", () => { + test("空预览视图应跳过处理", () => { + expect(() => { + scrollSynchronizer.handleEditorScroll(mockEditorView, null); + }).not.toThrow(); }); - test("scrollToBottom 应正确滚动到底部", () => { - const instance = scrollSynchronizer; - const targetElement = document.createElement("div"); - const content = document.createElement("div"); - targetElement.appendChild(content); - targetElement.style.height = "100px"; - content.style.height = "200px"; - targetElement.scrollTop = 50; - instance["scrollToBottom"](targetElement); + test("无效文档状态处理", () => { + const invalidEditorView = { ...mockEditorView, state: null } as any; + expect(() => { + scrollSynchronizer.handleEditorScroll(invalidEditorView, mockPreviewView); + }).not.toThrow(); + }); + }); - waitFor(() => { - expect(targetElement.scrollTop).toBe(200 - 100); - }); + describe("动画逻辑", () => { + test("滚动到底部应触发动画", () => { + const mockRequestAnimationFrame = vi.spyOn(window, "requestAnimationFrame"); + scrollSynchronizer["scrollToBottom"](mockPreviewView); + + expect(mockRequestAnimationFrame).toHaveBeenCalled(); + }); + + test("滚动过程中断应安全处理", () => { + const element = document.createElement("div"); + const child = document.createElement("div"); + element.appendChild(child); + child.style.height = "1000px"; + element.style.height = "500px"; + element.scrollTop = 300; + + scrollSynchronizer["scrollToBottom"](element); + element.scrollTop = 0; // 模拟外部中断 + + // 验证无错误抛出 + expect(() => { + vi.advanceTimersByTime(200); + }).not.toThrow(); }); }); }); + +// 配置测试环境 +beforeAll(() => { + Object.defineProperty(HTMLElement.prototype, "clientHeight", { value: 500 }); + Object.defineProperty(HTMLElement.prototype, "scrollHeight", { value: 1000 }); + vi.useFakeTimers(); +}); + +afterAll(() => { + vi.useRealTimers(); +}); diff --git a/packages/mini-markdown-editor/src/utils/__test__/storage.test.ts b/packages/mini-markdown-editor/src/utils/__test__/storage.test.ts index cbcd4d9..9b6ceb5 100644 --- a/packages/mini-markdown-editor/src/utils/__test__/storage.test.ts +++ b/packages/mini-markdown-editor/src/utils/__test__/storage.test.ts @@ -22,7 +22,7 @@ const mockLocalStorage = (() => { }; })(); -describe("safeLocalStorage", () => { +describe("storage Utils测试", () => { let originalLocalStorage: Storage; const consoleError = vi.spyOn(console, "error"); const consoleWarn = vi.spyOn(console, "warn"); -- Gitee