diff --git a/packages/mini-markdown-editor/src/components/Sidebar/__test__/Contents.test.tsx b/packages/mini-markdown-editor/src/components/Sidebar/__test__/Contents.test.tsx
index 4df464548dcc3a67ed8acc197e567e96c1e8d027..00849cd2583fe30913ffbd7dd14d9f8455dfbba4 100644
--- a/packages/mini-markdown-editor/src/components/Sidebar/__test__/Contents.test.tsx
+++ b/packages/mini-markdown-editor/src/components/Sidebar/__test__/Contents.test.tsx
@@ -1,4 +1,4 @@
-import { render, screen, act } from "@testing-library/react";
+import { render, screen, act, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { vi, describe, test, expect, beforeEach } from "vitest";
import Contents from "../Contents";
@@ -84,16 +84,14 @@ describe("Contents 组件测试", () => {
test("内容变化时更新目录", async () => {
const { rerender } = render();
-
const mockH3 = document.createElement("h3");
mockH3.innerText = "Title 3";
mockH3.setAttribute("data-line", "3");
mockPreviewElement.appendChild(mockH3);
- //让组件重新渲染
- await act(async () => {
- mockPreviewElement.dispatchEvent(new Event("scroll"));
- });
rerender();
- expect(screen.getAllByRole("link")).toHaveLength(3);
+
+ waitFor(() => {
+ expect(screen.getAllByRole("link")).toHaveLength(3);
+ });
});
});
diff --git a/packages/mini-markdown-editor/src/components/Sidebar/__test__/Help.test.tsx b/packages/mini-markdown-editor/src/components/Sidebar/__test__/Help.test.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..63e3ba5678ca27fa9496f111eb6af7b2227e1a27
--- /dev/null
+++ b/packages/mini-markdown-editor/src/components/Sidebar/__test__/Help.test.tsx
@@ -0,0 +1,35 @@
+import { render, waitFor } from "@testing-library/react";
+import Help from "../Help";
+import { grammar, shortcuts } from "@/common/help";
+import { describe, it, expect } from "vitest";
+
+describe("Help 组件测试", () => {
+ it("应该正常渲染", () => {
+ const { getByText } = render();
+
+ expect(getByText("Markdown 语法")).toBeInTheDocument();
+ expect(getByText("快捷键")).toBeInTheDocument();
+ });
+
+ it("应该渲染语法规则", () => {
+ const { getByText } = render();
+
+ grammar.forEach((rule) => {
+ waitFor(() => {
+ expect(getByText(rule.title)).toBeInTheDocument();
+ expect(getByText(rule.rule)).toBeInTheDocument();
+ });
+ });
+ });
+
+ it("应该渲染快捷键", () => {
+ const { getByText } = render();
+
+ shortcuts.forEach((shortcut) => {
+ waitFor(() => {
+ expect(getByText(shortcut.title)).toBeInTheDocument();
+ expect(getByText(shortcut.rule)).toBeInTheDocument();
+ });
+ });
+ });
+});
diff --git a/packages/mini-markdown-editor/src/components/Sidebar/__test__/Output.test.tsx b/packages/mini-markdown-editor/src/components/Sidebar/__test__/Output.test.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..d21ef4e5150d7e6d7856a6cf120d8d4ec3cbb2d7
--- /dev/null
+++ b/packages/mini-markdown-editor/src/components/Sidebar/__test__/Output.test.tsx
@@ -0,0 +1,64 @@
+import { render, screen, fireEvent, waitFor } from "@testing-library/react";
+import { describe, it, expect, vi, beforeAll } from "vitest";
+import Output from "../Output";
+import { exportHTML } from "@/utils/output-html";
+import { exportPdf } from "@/utils/output-pdf";
+
+beforeAll(() => {
+ window.matchMedia =
+ window.matchMedia ||
+ function () {
+ return {
+ matches: false,
+ addListener: function () {},
+ removeListener: function () {},
+ };
+ };
+});
+
+vi.mock("@/store/editor", () => ({
+ useEditorContentStore: vi.fn(() => "
Preview Content
"),
+}));
+
+vi.mock("@/utils/output-html", () => ({
+ exportHTML: vi.fn(),
+}));
+
+vi.mock("@/utils/output-pdf", () => ({
+ exportPdf: vi.fn(),
+}));
+
+describe("Output 组件测试", () => {
+ it("正确渲染", () => {
+ render();
+ expect(screen.getByText("导出文件类型")).toBeInTheDocument();
+ expect(screen.getByText("导出文件名")).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: "导 出" })).toBeInTheDocument();
+ });
+
+ it("正确调用PDF导出函数", async () => {
+ render();
+ fireEvent.change(screen.getByPlaceholderText("请填入文件名"), {
+ target: { value: "test-file" },
+ });
+ fireEvent.click(screen.getByText("PDF"));
+ fireEvent.click(screen.getByRole("button", { name: "导 出" }));
+ waitFor(() => {
+ expect(exportPdf).toHaveBeenCalledWith("Preview Content
", "test-file");
+ });
+ });
+
+ it("正确调用html导出函数", async () => {
+ render();
+ fireEvent.change(screen.getByPlaceholderText("请填入文件名"), {
+ target: { value: "test-file" },
+ });
+ fireEvent.mouseDown(screen.getByRole("combobox", { name: "导出文件类型" }));
+
+ waitFor(() => {
+ fireEvent.click(screen.getByRole("option", { name: "HTML" }));
+ fireEvent.click(screen.getByRole("button", { name: "导 出" }));
+ expect(exportHTML).toHaveBeenCalledWith("Preview Content
", "test-file");
+ });
+ });
+});
diff --git a/packages/mini-markdown-editor/src/components/Status/__test__/index.test.tsx b/packages/mini-markdown-editor/src/components/Status/__test__/index.test.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..dece986ea5599b5d8c135d8981024cfaecc7d392
--- /dev/null
+++ b/packages/mini-markdown-editor/src/components/Status/__test__/index.test.tsx
@@ -0,0 +1,56 @@
+import { render, screen, fireEvent } from "@testing-library/react";
+import { describe, it, expect, vi } from "vitest";
+import Status from "../index";
+import { useEditorContentStore } from "@/store/editor";
+import { handleScrollTop } from "@/utils/handle-scroll";
+vi.mock("@/store/editor", () => ({
+ useEditorContentStore: vi.fn(),
+}));
+// Mock 工具函数
+vi.mock("@/utils/handle-scroll", () => ({
+ handleScrollTop: vi.fn(),
+}));
+describe("Status 组件测试", () => {
+ it("正确展示内容字数", () => {
+ vi.mocked(useEditorContentStore).mockReturnValue({
+ content: "Hello World",
+ editorView: null,
+ previewView: null,
+ });
+
+ render();
+
+ expect(screen.getByText("字数: 10")).toBeInTheDocument();
+ });
+
+ it("当checkbox被点击时应该调用updateSyncScrollStatus", () => {
+ const updateSyncScrollStatus = vi.fn();
+ vi.mocked(useEditorContentStore).mockReturnValue({
+ content: "",
+ editorView: null,
+ previewView: null,
+ });
+
+ render();
+
+ const checkbox = screen.getByRole("checkbox");
+ fireEvent.click(checkbox);
+
+ expect(updateSyncScrollStatus).toHaveBeenCalledWith(true);
+ });
+
+ it("当scroll-top被点击时应该调用handleScrollTop", () => {
+ vi.mocked(useEditorContentStore).mockReturnValue({
+ content: "",
+ editorView: {},
+ previewView: {},
+ });
+
+ render();
+
+ const scrollTopButton = screen.getByText("滚动到顶部");
+ fireEvent.click(scrollTopButton);
+
+ expect(handleScrollTop).toHaveBeenCalled();
+ });
+});
diff --git a/packages/mini-markdown-editor/src/components/providers/__test__/config-provider.test.tsx b/packages/mini-markdown-editor/src/components/providers/__test__/config-provider.test.tsx
deleted file mode 100644
index 95e417ed25fceb66aa70c6dad9b679df8a450411..0000000000000000000000000000000000000000
--- a/packages/mini-markdown-editor/src/components/providers/__test__/config-provider.test.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import { render, screen } from "@testing-library/react";
-import { describe, it, expect } from "vitest";
-import { ConfigProvider, ConfigContext } from "../config-provider";
-import { defaultGlobalConfig } from "@/config/global";
-import { GlobalConfig } from "@/types/global-config";
-import { useContext } from "react";
-
-// 创建一个测试组件来消费 Context
-const TestComponent = () => {
- const config = useContext(ConfigContext);
- return {JSON.stringify(config)}
;
-};
-
-describe("ConfigProvider 组件测试", () => {
- it("应该提供默认的配置", () => {
- render(
-
-
- ,
- );
-
- const displayedConfig = screen.getByTestId("config-value").textContent;
- expect(displayedConfig).toEqual(JSON.stringify(defaultGlobalConfig));
- });
-
- it("应该支持自定义配置", () => {
- const customConfig: GlobalConfig = {
- theme: "dark",
- //不需本体存储
- local: false,
- };
-
- render(
-
-
- ,
- );
-
- const displayedConfig = screen.getByTestId("config-value").textContent;
- expect(displayedConfig).toContain('"theme":"dark"');
- expect(displayedConfig).toContain('"local":false');
- });
-
- it("自定义配置应与默认配置合并", () => {
- const customConfig: Partial = {
- theme: "dark",
- };
-
- render(
-
-
- ,
- );
-
- const displayedConfig = screen.getByTestId("config-value").textContent;
-
- // 确保 "theme" 覆盖默认值
- expect(displayedConfig).toContain('"theme":"dark"');
-
- // 但其他默认配置仍然存在
- Object.keys(defaultGlobalConfig).forEach((key) => {
- if (key !== "theme") {
- expect(displayedConfig).toContain(`"${key}":`);
- }
- });
- });
-});
diff --git a/packages/mini-markdown-editor/src/components/providers/__test__/toolbar-provider.test.tsx b/packages/mini-markdown-editor/src/components/providers/__test__/toolbar-provider.test.tsx
deleted file mode 100644
index 2a26151d465a0a57f98fb786c6ad56b0210e0b1f..0000000000000000000000000000000000000000
--- a/packages/mini-markdown-editor/src/components/providers/__test__/toolbar-provider.test.tsx
+++ /dev/null
@@ -1,92 +0,0 @@
-import { render, screen } from "@testing-library/react";
-import { useContext, useEffect, useState } from "react";
-import { describe, test, expect, vi, beforeEach } from "vitest";
-import { ToolbarProvider, ToolbarContext } from "../toolbar-provider";
-import type { ToolbarContextValues, ToolbarItem } from "@/types/toolbar";
-import { toolbarConfig } from "@/config/toolbar";
-// 模拟工具栏配置模块
-vi.mock("@/config/toolbar", () => ({
- toolbarConfig: {
- getAllToolbars: vi.fn(() => [
- { type: "file", title: "文件" },
- { type: "edit", title: "编辑" },
- ]),
- },
-}));
-// 测试用消费者组件
-const TestConsumer = () => {
- const context = useContext(ToolbarContext);
- const [testItems, setTestItems] = useState([]);
-
- useEffect(() => {
- if (context?.toolbars) {
- setTestItems(context.toolbars);
- }
- }, [context]);
-
- return (
-
-
{testItems.length}
-
{testItems[0]?.title}
-
- );
-};
-
-describe("ToolbarProvider 组件测试", () => {
- beforeEach(() => {
- vi.clearAllMocks();
- });
-
- test("应正确初始化工具栏配置", async () => {
- // 渲染组件
- render(
-
-
- ,
- );
-
- // 验证初始化调用
- expect(await screen.findByTestId("toolbar-count")).toHaveTextContent("2");
- expect(await screen.findByTestId("first-toolbar")).toHaveTextContent("文件");
- });
-
- test("应提供有效的上下文值", () => {
- let contextValue: ToolbarContextValues = { toolbars: [] };
- // 直接访问上下文的辅助组件
- const ContextChecker = () => {
- contextValue = useContext(ToolbarContext)!;
- return null;
- };
-
- render(
-
-
- ,
- );
-
- expect(contextValue).toBeDefined();
- expect(contextValue?.toolbars).toHaveLength(2);
- expect(contextValue?.toolbars[1].type).toBe("edit");
- });
-
- test("应只在挂载时加载配置", async () => {
- const { rerender } = render(
-
-
- ,
- );
-
- // 初始调用
- expect(vi.mocked(toolbarConfig.getAllToolbars)).toHaveBeenCalledTimes(1);
-
- // 重新渲染
- rerender(
-
-
- ,
- );
-
- // 验证没有重复调用
- expect(toolbarConfig.getAllToolbars).toHaveBeenCalledTimes(1);
- });
-});
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
new file mode 100644
index 0000000000000000000000000000000000000000..6d909667afddabcb5e60f651b72c766d030118af
--- /dev/null
+++ b/packages/mini-markdown-editor/src/hooks/__test__/use-copy-code.test.tsx
@@ -0,0 +1,151 @@
+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 { useCopyCode } from "../use-copy-code";
+
+// Mock ReactDOM.createRoot 和定时器
+vi.mock("react-dom/client", () => ({
+ createRoot: vi.fn(() => ({
+ render: vi.fn(),
+ unmount: vi.fn(),
+ })),
+}));
+
+// Mock requestAnimationFrame
+vi.stubGlobal("requestAnimationFrame", (fn: FrameRequestCallback) => setTimeout(fn, 0));
+
+// 测试组件容器
+const TestComponent = ({ node }: { node: string }) => {
+ const previewRef = useRef(null);
+ useCopyCode({ previewRef, node });
+ return ;
+};
+
+describe("useCopyCode Hook测试", () => {
+ beforeEach(() => {
+ vi.useFakeTimers();
+ // 重置DOM
+ document.body.innerHTML = "";
+ vi.clearAllMocks();
+ });
+ afterEach(() => {
+ vi.clearAllTimers();
+ });
+
+ test("应在挂载时添加复制按钮", async () => {
+ // 准备测试DOM结构
+ const preview = document.createElement("div");
+ preview.innerHTML = `
+
+
+
console.log('test')
+
+ `;
+ document.body.appendChild(preview);
+
+ const { unmount } = render(, {
+ container: preview,
+ });
+
+ // 等待异步操作完成
+ await act(async () => {
+ vi.runAllTimers();
+ });
+
+ // 验证按钮容器已添加
+ const buttons = preview.querySelectorAll(".copy-code-button-wrapper");
+ waitFor(() => {
+ expect(buttons.length).toBe(1);
+ // 验证createRoot调用
+ expect(createRoot).toHaveBeenCalledTimes(1);
+ });
+
+ unmount();
+ });
+
+ test("应在node变化时清理并重新创建按钮", async () => {
+ const preview = document.createElement("div");
+ preview.innerHTML = `
+
+
+
console.log('updated')
+
+ `;
+ const { rerender } = render(, {
+ container: preview,
+ });
+
+ // 初始渲染
+ await act(async () => {
+ vi.runAllTimers();
+ });
+
+ rerender();
+
+ await act(async () => {
+ vi.runAllTimers();
+ });
+ // 验证清理函数被调用
+ const mockRoot = createRoot({} as HTMLElement);
+ waitFor(() => {
+ expect(mockRoot.unmount).toHaveBeenCalled();
+ expect(createRoot).toHaveBeenCalledTimes(2);
+ });
+ });
+
+ test("应在卸载时执行清理", async () => {
+ const preview = document.createElement("div");
+ const { unmount } = render(, {
+ container: preview,
+ });
+
+ await act(async () => {
+ vi.runAllTimers();
+ });
+
+ // 执行卸载
+ unmount();
+
+ // 验证清理函数被调用
+ const mockRoot = createRoot({} as HTMLElement);
+ waitFor(() => {
+ expect(mockRoot.unmount).toHaveBeenCalled();
+ });
+ });
+
+ test("应处理无匹配元素的情况", async () => {
+ const preview = document.createElement("div");
+ preview.innerHTML = ``;
+ render(, {
+ container: preview,
+ });
+
+ await act(async () => {
+ vi.runAllTimers();
+ });
+
+ expect(createRoot).not.toHaveBeenCalled();
+ });
+
+ test("应跳过已存在按钮的元素", async () => {
+ const preview = document.createElement("div");
+ preview.innerHTML = `
+
+ `;
+ render(, {
+ container: preview,
+ });
+
+ await act(async () => {
+ vi.runAllTimers();
+ });
+
+ expect(createRoot).not.toHaveBeenCalled();
+ });
+});