首页
/ Ghost Koenig Lexical 编辑器开发与测试工作流指南:从 AGENTS 规范到 Playwright/Vitest 工程实践

Ghost Koenig Lexical 编辑器开发与测试工作流指南:从 AGENTS 规范到 Playwright/Vitest 工程实践

2026-09-07 11:27:53作者:明树来

Koenig Lexical(@tryghost/koenig-lexical)是 Ghost 基于 Meta Lexical 框架实现的富文本文章编辑器。本文以仓库内 koenig/koenig-lexical/AGENTS.md 为核心脉络,系统梳理该包面向开发者与 AI Agent 的统一工作流:pnpm 命令约定、单元测试与验收测试的组织方式、"AI 友好型测试"的默认行为,以及如何把编辑器以独立模式或集成模式跑起来并完成提交流程。

1. Koenig Lexical 在 Ghost 中的位置

Koenig 是 Ghost 的卡片化编辑器体系,而 koenig-lexical 是其中基于 Lexical 的最新实现(package.json 中描述为 "Ghost's Lexical-based rich text post editor",版本号示例为 1.9.4)。它与 koenig 目录下的多个工作区包协同工作:

在修改本包前,务必先阅读 koenig/koenig-lexical/README.md,尤其是其中的 Development、Testing 与 Editor integration 三个章节——这是 AGENTS.md 明文要求的首要动作。另外同目录下的 koenig/koenig-lexical/CLAUDE.md 与 AGENTS.md 内容一致,供不同 Agent 工具链读取同一套规则。

2. 必守工作流:pnpm 与测试分层

AGENTS.md 定义了本包开发的"必要工作流"(Required workflow),核心约束有三条。

2.1 统一使用 pnpm

整个 Ghost 仓库是 pnpm workspace monorepo(见根目录 pnpm-workspace.yamlpackage.json),因此任何安装、构建、测试命令都必须以 pnpm 执行,不要混用 npm/yarn。

2.2 理解两层测试体系的分工

AGENTS.md 特别强调测试分层,防止混淆:

  • 本包 package.json 中的 Playwright 测试是包级验收测试(package-level acceptance tests),验证编辑器在真实浏览器中的行为,测试文件位于 koenig/koenig-lexical/test/e2e/
  • Ghost 真正的浏览器 端到端(E2E)套件位于仓库顶层的 e2e/ workspace(即 e2e/ 目录),面向完整产品流程。

改动编辑器后,应根据影响范围选择在包级运行验收测试,或在仓库级跑 E2E,二者不可互相替代。

2.3 先聚焦后全量,提交前双门禁

推荐的迭代节奏是:

  1. 开发期间用 pnpm test:unit:watch 进行聚焦的单元测试开发(watch 模式,改动即重跑);
  2. 需要精简验收测试输出时用 pnpm test:acceptance:quiet(只显示失败用例);
  3. 反复运行相关的聚焦测试直到通过;
  4. 提交前最终执行 pnpm testpnpm lint 全部通过。

3. AI 友好型测试:默认行为的工程细节

AGENTS.md 中最具特色的部分是 "AI-Friendly Testing"——测试运行器被专门配置为对 AI Agent 友好,确保机器执行时的确定性。其四项默认行为都可以在配置中找到对应实现。

3.1 默认无头模式 + list reporter

AGENTS.md 规定默认行为是 Headless(无浏览器 UI、不打开网页),配合 list reporter 输出清晰的 pass/fail 信息。对应实现位于 koenig/koenig-lexical/playwright.config.ts

reporter: process.env.CI ? [['github'], ['html']] :
          process.env.PLAYWRIGHT_HTML_REPORT ? [['html'], ['list']] :
          [['list']],

headless 则读取 PLAYWRIGHT_HEADED 环境变量:

headless: process.env.PLAYWRIGHT_HEADED ? false : true

也就是说:不加任何环境变量时,浏览器静默运行、只输出 list 报告;这正是"AI 可直接解读输出、无挂起进程、不弹浏览器窗口"的原因。

3.2 静默模式与干净退出

  • pnpm test:acceptance:quiet 实际执行的是 pnpm test:acceptance --reporter=line(见 package.json 的 scripts),把输出压缩到只显示失败;
  • 测试完成即退出,不会遗留浏览器进程或报告服务器——因为只有显式指定 PLAYWRIGHT_HTML_REPORT=true 时才启用 html reporter,--ui / headed 模式也只是在需要交互式调试时才开启。

3.3 何时才允许交互式调试

AGENTS.md 明确列出三个"仅在交互式调试有用时才使用"的命令,且验证完毕后不得残留浏览器或报告服务器

  • pnpm test:acceptance:headed:带可见浏览器的验收测试;
  • pnpm test:acceptance --ui:Playwright UI 模式(watch 调试);
  • pnpm test:slowmo:带 100ms 步进延迟的 headed 测试。

以 slowmo 为例,它的实现会同时放大超时阈值以避免误报超时:

"test:slowmo": "TIMEOUT=100000 PLAYWRIGHT_SLOWMO=100 pnpm test:acceptance:headed"

在 playwright 配置中,PLAYWRIGHT_SLOWMO 通过 launchOptions.slowMo 注入浏览器(playwright.config.ts),并且在 CI 环境默认启用 2 次重试与 forbidOnly(防止 test.only 被误提交)。

4. 从 AGENTS 指令还原出的完整命令矩阵

AGENTS.md 只点出了最常用的 4 个命令,而完整矩阵定义在 koenig/koenig-lexical/package.json 与 README 中,建议组合使用:

命令 作用 适用时机
pnpm test 运行全部测试(unit + acceptance)后退出 提交前全量回归
pnpm test:unit typecheck + 运行单元测试 快速校验逻辑
pnpm test:unit:watch watch 模式,文件变更自动重跑单元测试 聚焦单元测试开发
pnpm test:unit:watch -t "buildCardMenu" describe/it 关键字过滤测试 只跑某组用例
pnpm test:acceptance 无头运行 Playwright 验收测试 默认验收回归
pnpm test:acceptance:quiet --reporter=line,仅输出失败 精简输出(AI 场景推荐)
pnpm test:acceptance:headed 带浏览器运行 交互式调试
pnpm test:acceptance:report 生成 HTML 报告 CI 失败分析
pnpm test:slowmo 100ms 慢动作 headed 模式 观察逐步交互
pnpm lint 依次运行 lint:csslint:js 提交前门禁
pnpm typecheck tsc --noEmit -p tsconfig.typecheck.json 类型检查

4.1 运行前置条件:pretest 钩子

在开始测试之前,包会先构建一份 demo 应用用于测试,这由 pretest 钩子完成:

"pretest": "VITE_TEST=true pnpm build --config vite.config.demo.ts && pnpm build"

因此首次跑 pnpm test 时会先产出 demo 构建产物再启动测试,请勿在构建阶段手动中断。

4.2 ESM 环境下的验收测试约束

README 特别提醒:由于 package.json 声明了 "type": "module",验收测试运行在 Node ESM 模式下,会遇到三类限制——不能使用 require/__filename 等 CommonJS 全局量、导入必须带完整文件扩展名、不能依赖 require.extensions。包内通过 Playwright 脚本注入两个参数来缓解:

"test:acceptance": "NODE_ENV=test PLAYWRIGHT_FORCE_ASYNC_LOADER=1 NODE_OPTIONS='--experimental-specifier-resolution=node --no-warnings' VITE_TEST=true playwright test"

其中 PLAYWRIGHT_FORCE_ASYNC_LOADER=1 的来龙去脉在 playwright.config.ts 的注释中有详细解释:Node 22.15+ 的同步模块钩子无法处理"对 ESM-only 包的 CJS require()"(jsdom 29 依赖链会报 "request for X is not in cache"),而异步 loader 路径可正常处理;Node 24.x 已修复,待 workspace 升级后可移除该标志。实践中若某些常量恰好放在 .jsx 文件里导致 ESM 导入报错,处理方式是将其移动到 .js 文件。

5. 单元测试与验收测试的工程组织

5.1 测试目录约定

测试遵循统一布局(见 koenig/koenig-lexical/test):

  • test/unit/:Vitest 单元测试,如 CardMenu.test.tsxbuildCardMenu.test.tshooks/useVisibilityToggle.test.tsutils/generateEditorState.test.ts
  • test/e2e/:Playwright 验收测试,又按行为分组组织:
    • test/e2e/cards/:每个卡片一个文件(audio、bookmark、button、callout、toggle、signup、product、image、html 等);
    • test/e2e/editors/:basic-editor、email-editor、minimal-editor;
    • test/e2e/plugins/:DragDrop、EmojiPicker、WordCount 等插件行为;
    • test/e2e/text-transforms/:文本变换规则;
    • 以及 card-behaviour.test.tspaste-behaviour.test.tsfloating-toolbar.test.tsslash-menu.test.tslist-behaviour.test.ts 等总览级用例;
  • test/utils/:被两层测试共享的辅助工具,入口为 e2e.ts
  • test/test-setup.ts:Vitest 的全局 setup(通过 vite 配置中的 setupFiles 注入)。

两个浏览器项目(chromium + firefox)的划分也很清晰:默认跑 chromium,凡文件名带 firefox 标记的用例(如 video-card.firefox.test.tsDragDropPastePlugin.firefox.test.ts)只匹配 Firefox 项目(见 playwright.config.ts)。如果你在给这些用例补覆盖,请留意 testIgnore/testMatch 的正则命名约定。

5.2 共享测试辅助:从初始化到断言

包级验收测试高度依赖 koenig/koenig-lexical/test/utils/e2e.ts,理解这些辅助函数有助于编写风格一致的用例:

  • initialize({page}):导航到 http://localhost:5174(端口来自 playwright.config 导出的 E2E_PORT),固定 1000×1000 视口,等待 .koenig-lexical 渲染,然后把 Lexical 实例暴露到 window.lexicalEditor 上供后续读取;重复导航则通过 React Router 复用页面并重置编辑器状态;
  • insertCard({cardName, nth}):通过键入 /卡片名 触发斜杠菜单并回车插入卡片,随后按 data-kg-card 断言卡片可见——这是所有卡片验收测试的通用入口;
  • paste / pasteText / pasteHtml / pasteLexical / pasteFiles:以构造 ClipboardEvent 的方式模拟不同 MIME 类型的粘贴行为,覆盖 text/plaintext/htmlapplication/x-lexical-editor 与文件粘贴(fixtures 目录里准备了损坏/正常的 mp3、mp4、图片以覆盖失败路径);
  • assertHTML({selector, ignoreClasses, ignoreInlineStyles, ...}):对编辑器 innerHTML 做规整化(去掉 class、行内样式、SVG 内容、base64 数据、拖拽属性等)后再做快照比对,使断言对与业务无关的细节不敏感;
  • assertRootChildren / getEditorStateJSON:直接比对 Lexical EditorState 的 JSON 结构;
  • selectBackwards / selectForward / dragMouse / createDataTransfer:模拟键盘选区与鼠标拖拽交互。

例如一个典型的标题测试可写作:

await initialize({page});
await focusEditor(page);
await page.keyboard.type('# 标题');
await assertHTML(page, html`<h1>标题</h1>`);

5.3 单元测试的 jsdom 环境与关键字过滤

单元测试配置在 koenig/koenig-lexical/vite.config.tstest 段:globals: true(供 jest-dom 扩展使用)、environment: 'jsdom'setupFiles: './test/test-setup.ts'、默认 10s 超时,CI 下限制线程数避免资源竞争。由于预构建了 demo 版本用于单测,开发时可配合 -t 关键字快速圈定用例:

pnpm test:unit:watch -t "buildCardMenu"

6. 两种开发模式:Standalone 与 Integrated

虽然 AGENTS.md 没有展开运行细节,但其要求先读的 README 明确了编辑器的两种运行形态,这也是测试得以开展的前提。

6.1 Standalone(独立演示模式)

pnpm dev

该命令并行启动 Vite dev server、包构建 watch、preview 服务器,以及 kg-default-nodes/kg-default-transforms 的 watch 构建(见 package.jsondev 脚本)。编辑器演示页运行在 http://localhost:5173,由 index.html 渲染 demo/demo.tsx 中的全功能空编辑器(demo 应用源码在 koenig/koenig-lexical/demo 下,含 DemoApp、HtmlOutputDemo、RestrictedContentDemo 等演示视图)。

特殊卡片本地配置

  • Gif 卡片:需要在 koenig-lexical 包根目录新建 .env.local 并提供 Klipy API key:
    VITE_KLIPY_API_KEY=xxx
    
  • Bookmark / Embed 卡片:这两类卡片会发起外部网络请求,而 demo 没有后端服务,需要在前端直接跨域抓取资源,因此要启用 CORS(README 建议使用浏览器扩展如 'Test CORS');否则会看到被浏览器拦截的请求错误。也可以直接使用测试数据、跳过 fetchEmbed.js 的抓取逻辑来规避。

6.2 Integrated(集成到 Ghost Admin)

由于所有 Koenig 包都属于 Ghost monorepo workspace,无需手动 link,在仓库根目录运行:

pnpm dev:lexical

它启动常规 Ghost 开发环境,外加编辑器的 dev:integrated 目标:为编辑器(连同 kg-default-nodes / kg-default-transforms)启动 rebuild watcher,并在 4173 端口启动 preview 服务器;dev gateway 会把 Admin 的 EDITOR_URL 代理到这个 preview 服务。

随后访问 http://localhost:2368/ghost 打开任意文章,Admin 就会加载你本地构建的编辑器版本——改动会在几秒内(编辑器重建完成后)反映到 Ghost Admin 中。这是验证"编辑器真正集成进后台"的推荐路径。

7. 提交前检查单与规范维护

AGENTS.md 给出了收敛式的规范维护策略:

  1. 最终验证:在反复运行聚焦测试后,提交前必须通过 pnpm testpnpm lint
  2. 不要重复记录规范:当包的共享命令或测试工作流发生变更时,更新对象是 koenig/koenig-lexical/README.md(其 Testing 章节维护了命令矩阵与环境变量 PLAYWRIGHT_HEADEDPLAYWRIGHT_HTML_REPORTPLAYWRIGHT_SLOWMO 的完整说明),而不要在 AGENTS.md 里复制一份导致双源漂移;
  3. 发布流程:Koenig 系列包作为 Ghost monorepo workspace 的一部分统一版本化与发布,发布细节见 koenig/README.md 的 shipping 章节。

8. 小结:把这套工作流落到日常开发

从 AGENTS.md 出发可以提炼出 Koenig Lexical 开发的三条黄金法则:

  1. 命令走 pnpm,分层跑测试——单元测试 pnpm test:unit:watch 快速迭代,验收测试用 pnpm test:acceptance:quiet 精准反馈,最终 pnpm test && pnpm lint 收口;浏览器级 E2E 交给仓库顶层 e2e/ workspace,勿与包级 Playwright 用例混淆。
  2. 默认行为面向确定性——无头 + list reporter + 干净退出让 AI Agent 也能稳定驱动;headed / --ui / slowmo 仅用于需要肉眼看交互的时刻,用完即关。
  3. 规范单一来源——共享命令与工作流变更写进 README,AGENTS.md 保持精简,避免两处维护产生分歧。

对 AI 驱动的代码生成场景而言,这套规范本身就是一份高质量的"机器可读需求文档":先读 README 建立背景,用 watch/quiet 模式获得最小反馈环,再以全量测试与 lint 作为提交门禁——这正是 Ghost 编辑器团队在 koenig/koenig-lexical/AGENTS.md 中希望每个协作者(无论人类还是 Agent)遵守的协作契约。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.81 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
920
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.79 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
390