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 = `
-
- `;
- 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