- 前端
- 富文本
- UI组件
- AI 应用
【免费下载链接】BlockNote
A React Rich Text Editor that's block-based (Notion style) and extensible. Built on top of Prosemirror and Tiptap.
导读
本文围绕 BlockNote 仓库中一个特殊的基础示例examples/01-basic/testing展开:它不是一个面向用户的演示应用,而是专门为端到端(e2e)测试设计的“测试编辑器”,被仓库的 Playwright 浏览器测试套件直接挂载使用。读完本文,你将理解该示例的最小实现(含 base64 文件上传兜底逻辑)、它如何通过@examples别名被 basics.test.tsx 等测试导入渲染、测试工具层(选择器常量、等待/快照/截图断言)如何工作,以及整套 e2e 测试的运行方式与多浏览器实例配置。
一、testing 示例的定位:为 e2e 测试而生的最小编辑器
在 BlockNote 仓库的 examples/01-basic 目录下,绝大多数示例(01-minimal、02-block-objects、03-multi-column等)都承担着“向开发者展示某类 API 用法”的文档职责。而 testing/README.md 用两句话交代了它的唯一使命:
This example is meant for use in end-to-end tests.
也就是说,这个示例不是给人看的功能演示,而是给机器跑的测试基座。它的价值体现在两方面:
- 提供一个稳定、无副作用的编辑器宿主:测试需要反复挂载、卸载编辑器实例,因此该示例不包含任何外部依赖(无后端上传、无协作服务、无 AI 调用),保证任何浏览器环境下渲染结果一致;
- 与测试基础设施解耦:测试代码通过路径别名导入示例组件,示例自身不感知测试逻辑,职责单一。
这一点可以从 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/mantine的BlockNoteView与@blocknote/mantine/style.css:使用 Mantine 主题的编辑器 UI 外壳;@blocknote/react的useCreateBlockNote: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 标准链路:
render(<App />):来自vitest-browser-react,直接把示例组件渲染进真实浏览器 DOM(测试运行在浏览器中,而非 jsdom);waitForSelector(EDITOR_SELECTOR):轮询等待.bn-editor节点挂载,确保 ProseMirror 视图初始化完成;userEvent.click+userEvent.keyboard:模拟真实点击与键盘输入"hello world";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.ProseMirror的getJSON(),返回 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 实例,这是理解“测试编辑器为何要在多种环境下保持一致”的关键:
| 实例 | 浏览器 | 视口 | 说明 |
|---|---|---|---|
chromium | Chromium | 1280×720 | 桌面主实例,附加--no-sandbox等容器运行参数 |
firefox | Firefox | 1280×720 | 桌面兼容性 |
webkit | WebKit | 1280×720 | 桌面兼容性 |
android | Chromium(移动 UA) | 393×727 | 模拟 Android 12 / Chrome 151,isMobile: true, hasTouch: true,让 prosemirror-view 走 Android 输入路径 |
ios | WebKit(iPhone UA) | 393×727 | 模拟 iPhone / iOS 18 Safari,覆盖 iOS 输入路径(如原生 split 回读) |
桌面实例通过DESKTOP_EXCLUDE排除end-to-end/mobile/**,移动实例则用include限定各自专属套件。此外还做了全局兜底配置:
- 截图断言全局允许 2% 像素差异(
allowedMismatchedPixelRatio: 0.02),以吸收多浏览器反锯齿/字体渲染差异; testTimeout: 30000、retry: 2,缓解三浏览器共容器运行的偶发资源争抢;fileParallelism: false,单浏览器单 worker,避免 CPU 饱和导致超时。
5.3 测试前置与 iframe 尺寸
tests/vitestSetup.browser.ts 在每轮测试前完成两件重要工作:
- 将测试 iframe 尺寸设置为与浏览器窗口一致的 1280×720(移动实例为 393×727),避免 Vitest 默认 iframe 过窄导致菜单换行、截图失真;
- 注入样式
.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.
相关推荐
Multilingual-E5-large-instruct与其他嵌入模型对比:为什么选择它?
Multilingual E5 large instruct与其他嵌入模型对比:为什么选择它? Multilingual E5 large instruct是一
前端富文本UI组件AI 应用Base UI 端到端测试指南:Playwright + Vite 的 e2e 基础设施解析
Base UI 端到端测试指南:Playwright + Vite 的 e2e 基础设施解析 端到端(e2e)测试是验证 Base UI 组件在真实浏览器环境下
前端UI组件Kubernetes E2E 测试框架 test/e2e/framework 源码级解析:面向 Ginkgo 的端到端测试基础设施
Kubernetes E2E 测试框架 test/e2e/framework 源码级解析:面向 Ginkgo 的端到端测试基础设施 Kubernetes 仓库中
云原生容器编排集群管理微服务
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考