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 = ` +
+
+
+
+ existing button +
+ `; + render(, { + container: preview, + }); + + await act(async () => { + vi.runAllTimers(); + }); + + expect(createRoot).not.toHaveBeenCalled(); + }); +});