BlockNote 端到端测试专用示例编辑器:testing 示例的设计与 e2e 测试基础设施解析
2026/9/24 16:30:20 网站建设 项目流程
  • 前端
  • 富文本
  • UI组件
  • AI 应用

【免费下载链接】BlockNote

A React Rich Text Editor that's block-based (Notion style) and extensible. Built on top of Prosemirror and Tiptap.

项目地址:https://gitcode.com/gh_mirrors/bl/BlockNote
点击查看免费下载

导读

本文围绕 BlockNote 仓库中一个特殊的基础示例examples/01-basic/testing展开:它不是一个面向用户的演示应用,而是专门为端到端(e2e)测试设计的“测试编辑器”,被仓库的 Playwright 浏览器测试套件直接挂载使用。读完本文,你将理解该示例的最小实现(含 base64 文件上传兜底逻辑)、它如何通过@examples别名被 basics.test.tsx 等测试导入渲染、测试工具层(选择器常量、等待/快照/截图断言)如何工作,以及整套 e2e 测试的运行方式与多浏览器实例配置。

一、testing 示例的定位:为 e2e 测试而生的最小编辑器

在 BlockNote 仓库的 examples/01-basic 目录下,绝大多数示例(01-minimal02-block-objects03-multi-column等)都承担着“向开发者展示某类 API 用法”的文档职责。而 testing/README.md 用两句话交代了它的唯一使命:

This example is meant for use in end-to-end tests.

也就是说,这个示例不是给人看的功能演示,而是给机器跑的测试基座。它的价值体现在两方面:

  1. 提供一个稳定、无副作用的编辑器宿主:测试需要反复挂载、卸载编辑器实例,因此该示例不包含任何外部依赖(无后端上传、无协作服务、无 AI 调用),保证任何浏览器环境下渲染结果一致;
  2. 与测试基础设施解耦:测试代码通过路径别名导入示例组件,示例自身不感知测试逻辑,职责单一。

这一点可以从 tests/src/examples.d.ts 的注释中得到印证:e2e 测试通过import App from "@examples/<group>/<name>/src/App"挂载示例应用,@examples别名由 Vite 在运行时解析(见下文第五节)。

二、源码拆解:一个带文件上传兜底的最小编辑器

src/App.tsx 是整个示例的全部业务代码,仅 26 行:

import "@blocknote/core/fonts/inter.css"; import { BlockNoteView } from "@blocknote/mantine"; import "@blocknote/mantine/style.css"; import { useCreateBlockNote } from "@blocknote/react"; // "Uploads" a file by encoding it as a base64 data URL. In a real app you'd // replace this with an upload to your own backend that returns a URL to the // stored file. async function uploadFile(file: File) { return new Promise<string>((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result as string); reader.onerror = () => reject(reader.error); reader.readAsDataURL(file); }); } export default function App() { // Creates a new editor instance. const editor = useCreateBlockNote({ uploadFile, }); // Renders the editor instance using a React component. return <BlockNoteView editor={editor} />; }

2.1 三处导入:样式、UI 外壳与 React 桥接

  • @blocknote/core/fonts/inter.css:引入编辑器默认字体(Inter),保证测试环境下文字渲染与生产一致;
  • @blocknote/mantineBlockNoteView@blocknote/mantine/style.css:使用 Mantine 主题的编辑器 UI 外壳;
  • @blocknote/reactuseCreateBlockNote:React 侧创建编辑器实例的 Hook。

这与 examples/01-basic/01-minimal/src/App.tsx 等基础示例的引入方式保持一致,从源码结构看,testing示例正是以最小可运行形态覆盖默认编辑器能力。

2.2 uploadFile:base64 兜底的文件上传

uploadFile是本示例中唯一一段业务逻辑:用FileReader.readAsDataURL把文件编码为 base64 Data URL 后作为“上传结果”返回。源码注释明确说明——真实应用中应替换为上传到自有后端并返回存储 URL 的实现。它在测试中的意义在于:

  • 离线可用:e2e 测试(尤其是图片类测试)无需真实网络,直接以 Data URL 注入图片即可;
  • 结果可断言:测试可以通过检查img节点的src是否为data:开头来验证上传流程(参见 tests/src/end-to-end/images/images.test.tsx 所在目录的测试套件)。

2.3 入口与宿主

应用入口 main.tsx 用createRoot渲染<App />并包裹React.StrictMode,index.html 提供#root挂载点。这些文件都标注了“AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY”,说明示例脚手架由仓库脚本统一生成,保证所有示例结构一致。

三、测试如何挂载该示例:以 basics.test.tsx 为例

端到端测试套件位于 tests/src/end-to-end,共约数十个测试目录(basics、colors、comments、copypaste、dragdrop、tables、mobile 等)。其中 basics/basics.test.tsx 是直接挂载testing示例的最简案例:

import App from "@examples/01-basic/testing/src/App"; import { beforeEach, describe, expect, test } from "vite-plus/test"; import { render } from "vitest-browser-react"; import { userEvent } from "../../utils/context.js"; import { EDITOR_SELECTOR } from "../../utils/const.js"; import { waitForSelector } from "../../utils/editor.js"; beforeEach(async () => { await render(<App />); await waitForSelector(EDITOR_SELECTOR); }); describe("Basic typing functionality", () => { test("should allow me to type content", async () => { const editor = await waitForSelector(EDITOR_SELECTOR); await userEvent.click( document.querySelectorAll(`${EDITOR_SELECTOR} div`)[3] as HTMLElement, ); await userEvent.keyboard("hello world"); expect(editor.textContent).toBe("hello world"); }); });

这个测试完整走通了“渲染示例 → 等待编辑器就绪 → 模拟交互 → 断言结果”的 e2e 标准链路:

  1. render(<App />):来自vitest-browser-react,直接把示例组件渲染进真实浏览器 DOM(测试运行在浏览器中,而非 jsdom);
  2. waitForSelector(EDITOR_SELECTOR):轮询等待.bn-editor节点挂载,确保 ProseMirror 视图初始化完成;
  3. userEvent.click+userEvent.keyboard:模拟真实点击与键盘输入"hello world"
  4. expect(editor.textContent).toBe("hello world"):断言编辑器 DOM 文本与输入完全一致。

从源码结构看,userEvent来自 tests/src/utils/context.ts 的导出封装,它基于浏览器环境下的用户事件模拟实现。

四、测试工具层:选择器常量与断言工具

4.1 选择器常量(const.ts)

tests/src/utils/const.ts 集中定义了 e2e 测试依赖的 DOM 选择器,它们与 BlockNote 的 DOM 结构约定强相关。核心常量如下:

常量选择器用途
EDITOR_SELECTOR.bn-editor编辑器根节点,绝大多数测试的入口
BLOCK_CONTAINER_SELECTOR[data-node-type="blockContainer"]块容器
BLOCK_GROUP_SELECTOR[data-node-type="blockGroup"]块组
PARAGRAPH_SELECTOR[data-content-type="paragraph"]段落块
H_ONE_BLOCK_SELECTOR[data-content-type=heading]...各级标题块
IMAGE_SELECTOR/PDF_SELECTOR/TABLE_SELECTOR[data-content-type="image"/"pdf"/"table"]多媒体/表格块
DRAG_HANDLE_SELECTOR[data-test="dragHandle"]拖拽手柄
SLASH_MENU_SELECTOR.bn-suggestion-menu斜杠菜单(suggestion 菜单被传送到编辑器容器内,故按类名匹配)
ITALIC_BUTTON_SELECTOR[data-test="italic"]格式化工具栏按钮

这些选择器是测试与编辑器 DOM 契约的“公共接口”,任何 DOM 结构调整都会通过这些常量在测试中暴露。testing示例使用默认主题与默认块架构,因此这些选择器对它完全适用。

4.2 编辑器断言工具(editor.ts)

tests/src/utils/editor.ts 提供了可复用的等待与断言函数:

  • waitForSelector(selector, { timeout = 5000 }):基于vi.waitFor轮询,元素未出现时抛错,默认超时 5 秒;
  • waitForSelectorDetached:等待元素从 DOM 移除(用于菜单关闭、块删除等场景);
  • focusOnEditor:点击编辑器使其获得焦点;
  • waitForTextInEditor(text):等待编辑器文本包含指定内容;
  • getDoc():直接读取全局window.ProseMirrorgetJSON(),返回 ProseMirror 文档的 JSON 表示——源码注释特别说明测试运行在浏览器内,无需page.evaluate往返;
  • compareDocToSnapshot(name):将文档 JSON 与./__snapshots__/<name>.json快照比对,实现“文档级快照测试”;
  • expectElement(...)/matchPageScreenshot(name):视觉回归断言,前者可对任意元素做截图对比,后者对document.body整页截图(可捕获传送到 body 的菜单/工具栏)。

matchPageScreenshot的注释还透露了快照命名策略:视觉基线由 Vitest 按“浏览器 + 平台”自动命名,而文档 JSON 快照与浏览器无关,因此跨 chromium/firefox/webkit 三个实例共享同一份快照。

五、e2e 运行基础设施:vite.config.browser.ts 关键设计

整个 e2e 套件的运行配置集中在 tests/vite.config.browser.ts,其中与testing示例直接相关的机制包括:

5.1 @examples 别名与源码级解析

alias: { ...blockNoteSrcAliases, "@shared": path.resolve(__dirname, "../shared"), "@examples": path.resolve(__dirname, "../examples"), },
  • @examples指向仓库的examples目录,因此测试代码里import App from "@examples/01-basic/testing/src/App"会解析到 testing/src/App.tsx;
  • blockNoteSrcAliases把每个@blocknote/*包(core、react、mantine、shadcn 等)解析到各自的src/目录,即测试运行时直接从源码转译包代码,不依赖预先构建的 dist——这使修改包源码无需重建 Docker 镜像即可生效;
  • TypeScript 侧的配套声明在 tests/src/examples.d.ts:以环境模块declare module "@examples/*"声明默认导出为 React 组件,避免 tsc 下沉到示例源码破坏 composite 构建(TS6059)。

5.2 多浏览器实例矩阵

配置中注册了五个 Playwright 实例,这是理解“测试编辑器为何要在多种环境下保持一致”的关键:

实例浏览器视口说明
chromiumChromium1280×720桌面主实例,附加--no-sandbox等容器运行参数
firefoxFirefox1280×720桌面兼容性
webkitWebKit1280×720桌面兼容性
androidChromium(移动 UA)393×727模拟 Android 12 / Chrome 151,isMobile: true, hasTouch: true,让 prosemirror-view 走 Android 输入路径
iosWebKit(iPhone UA)393×727模拟 iPhone / iOS 18 Safari,覆盖 iOS 输入路径(如原生 split 回读)

桌面实例通过DESKTOP_EXCLUDE排除end-to-end/mobile/**,移动实例则用include限定各自专属套件。此外还做了全局兜底配置:

  • 截图断言全局允许 2% 像素差异(allowedMismatchedPixelRatio: 0.02),以吸收多浏览器反锯齿/字体渲染差异;
  • testTimeout: 30000retry: 2,缓解三浏览器共容器运行的偶发资源争抢;
  • fileParallelism: false,单浏览器单 worker,避免 CPU 饱和导致超时。

5.3 测试前置与 iframe 尺寸

tests/vitestSetup.browser.ts 在每轮测试前完成两件重要工作:

  1. 将测试 iframe 尺寸设置为与浏览器窗口一致的 1280×720(移动实例为 393×727),避免 Vitest 默认 iframe 过窄导致菜单换行、截图失真;
  2. 注入样式.bn-container { max-width: 731px; margin: 0 auto; padding-top: 8px; },与 BlockNote 官网示例页的编辑框宽度对齐,使截图基线与线上展示一致。

六、运行方式:脚本与 Docker 工作流

6.1 脚本入口

tests/package.json 提供了两条 e2e 命令:

{ "scripts": { "test:e2e": "vp test -c vite.config.browser.ts --run", "test:e2e:updateSnaps": "vp test -c vite.config.browser.ts --run -u" } }
  • test:e2e:以--run单次执行全部浏览器测试;
  • test:e2e:updateSnaps:追加-u更新所有文档 JSON 快照与视觉基线(新增/修改基线时使用)。

6.2 Docker 运行脚本(docker-run.sh)

tests/docker-run.sh 是官方推荐的本地执行方式。它的核心设计是“镜像只装依赖、源码运行时挂载”:

  • 镜像blocknote-e2e安装依赖但不构建任何包(见 tests/Dockerfile 配套说明);
  • 启动时把每个packages/*/src-v方式绑定挂载进容器,与 5.1 节的源码级解析配合,编辑包源码后无需重建镜像即可复测
  • 脚本会用内容哈希标签自动判断依赖/示例是否变化,必要时自动重建镜像;
  • 典型用法为tests/docker-run.sh [docker 参数] -- [vp 参数],例如tests/docker-run.sh -- -t "Basic typing"可只跑指定测试。

6.3 浏览器测试与单元测试的边界

需要说明的是,e2e 套件不仅包含tests/src/end-to-end/**/*.test.tsx,还会运行各包源码内联的*.browser.test.{ts,tsx}(如 canvas 栅格化、Mermaid 渲染等浏览器专属单测),见include配置:

include: [ "./src/end-to-end/**/*.test.tsx", "../packages/*/src/**/*.browser.test.{ts,tsx}", ],

这与testing示例无直接关系,但解释了为何该配置的include范围覆盖了包目录。

七、小结:从测试编辑器到测试契约

examples/01-basic/testing虽然只有一行业务组件,却在 BlockNote 的质量保障体系中扮演枢纽角色:

  • 对测试代码,它是稳定、可复现的渲染宿主(src/App.tsx);
  • 对编辑器 DOM,它通过 const.ts 中的选择器与测试建立“DOM 契约”,任何样式类或data-*属性的变更都会被测试直接捕获;
  • 对 CI,它与 vite.config.browser.ts 的源码级别名、五实例浏览器矩阵、快照断言共同构成可离线、可并发的回归防线。

若你希望在自有项目中复刻这套模式,最直接的做法是:仿照本示例维护一个“最小测试编辑器”应用,导出其 App 组件供测试挂载;将编辑器根节点、块类型、工具栏按钮的选择器集中到常量文件中;再用浏览器模式测试框架(本仓库使用 vite-plus + vitest-browser-react + Playwright)驱动真实输入与截图断言。当编辑器 DOM 契约稳定后,这套测试基座即可长期复用于功能回归与视觉回归。

相关文件索引

  • 示例本体:README.md、src/App.tsx、main.tsx
  • 直接挂载该示例的测试:basics.test.tsx
  • 测试工具层:const.ts、editor.ts、examples.d.ts
  • 运行配置:vite.config.browser.ts、vitestSetup.browser.ts、tests/package.json、docker-run.sh
  • 前端
  • 富文本
  • UI组件
  • AI 应用

【免费下载链接】BlockNote

A React Rich Text Editor that's block-based (Notion style) and extensible. Built on top of Prosemirror and Tiptap.

项目地址:https://gitcode.com/gh_mirrors/bl/BlockNote
点击查看免费下载
上一篇:Bitwarden server 如何用 k6 对登录端点做固定 QPS 在线压测并检查延迟阈值
下一篇:TCP Option Address (TOA)深度解析:如何从TCP头部选项高效提取源IPv4地址

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询