- 开发工具
- 后端
【免费下载链接】rstudio
RStudio is an integrated development environment (IDE) for R
本篇文章聚焦 RStudio 仓库中.claude/skills/rstudio-create-playwright-tests/visual-editor.md所沉淀的视觉(WYSIWYG)Markdown 编辑器测试模式,系统讲解如何用 Playwright 驱动 panmirror 可视化编辑器中的 Insert Citation 对话框、Insert/Format 菜单等 React UI。读完本文,你将掌握进入可视化模式的可靠姿势、React 渲染与 GWT 对话框的 DOM 层级差异、react-window 虚拟列表与一次性初始化重置的规避技巧、latent search 的真实触发方式,以及针对外部引文服务故障的"跳过而非失败"断言策略——这些模式在 citations.test.ts 等测试中已经过实战验证。
背景:为什么可视化编辑器需要一套独立的测试模式
RStudio 的编辑器提供两种 Markdown 编辑模式:源代码(source)模式基于 Ace 编辑器,而可视化(visual)模式则是所见即所得(WYSIWYG)的panmirror。panmirror 是一个 React 应用,其源码位于 Quarto 仓库(packages/editor/src/behaviors/...),不在当前 RStudio 仓库内;RStudio 侧只能通过 DOM 与 RPC 边界与它交互。
由此产生两个直接后果:
- 功能隔离:引文(Citations)与 Insert 菜单仅存在于可视化模式中,源代码模式下根本不可见;
- DOM 结构差异:panmirror 的 UI 由 React 渲染,其层级与 GWT 渲染的对话框外壳完全不同,沿用 GWT 时代的对话框作用域选择器会定位不到任何真实控件。
因此,所有与可视化编辑器交互的 Playwright 测试都必须遵循本文档沉淀的统一模式,而不是把源模式的选择器习惯生搬硬套。
进入可视化模式:ensureVisualMode()的正确用法
驱动引文功能的第一步是让编辑器进入可视化模式。推荐统一调用SourcePaneActions.ensureVisualMode(),它在 source_pane.actions.ts 中的实现体现了三个关键设计:
- 幂等探测:先检查
.ProseMirror是否已经挂载(可视化模式下编辑器会挂载 ProseMirror 表面),已处于可视化模式则直接返回; - 等待工具栏就绪:等待
rstudio_visual_md_on切换开关(定义于 source_pane.page.ts)可见,超时 5 秒内不出现则视为该文件类型不支持可视化模式并静默返回; - 处理首次切换的 pandoc 转换确认对话框:首次将文档切换到可视化模式时,pandoc 转换是服务端往返,可能触发一个确认对话框。实现不依赖固定轮询窗口,而是同时等待「对话框出现」或「编辑器挂载」二者之一(
proseMirror.or(okBtn),超时 90 秒),对话框出现则点击#rstudio_dlg_ok确认,随后再等待.ProseMirror真正挂载。
宿主的选取:测试中新建文档时,newMarkdownDoc命令(通过executeCommand触发)是最廉价的选择——它不需要rmarkdown包,也不会弹出 Quarto 新建文档向导。这一点在 citations.test.ts 的newMarkdownVisualDoc辅助函数中有明确注释,配合installDepIfPrompted处理可能出现的依赖安装提示,再用selectedTab断言确认未命名文档已打开后进入可视化模式。
panmirror 渲染在 GWT 对话框之外:选择器作用域是page而非dialog
这是最容易踩坑的一点:panmirror 的 UI(例如 Insert Citation 面板)渲染在一个 React 根节点中,这个根节点是 GWT[role="dialog"]外壳的兄弟节点,而不是它的子节点。
以 Insert Citation 对话框为例,insert_citation.page.ts 的注释明确指出:
- 导航树、搜索框、结果列表等 React 控件都是page 作用域(
page.locator(...)); - 只有 GWT 的底部按钮(Insert / Cancel)位于
[role="dialog"]内,通过this.dialog.getByRole('button', { name: 'Insert' })定位。
如果习惯性地把选择器限定在对话框作用域内(例如locator('button')或对整个对话框做outerHTML导出),得到的将是空结果或错误的 DOM 快照——真实控件根本不在那个子树里。规则:可视化编辑器相关的所有 React 控件,一律使用page级选择器。
Insert Citation 对话框:打开、选中与一次性初始化重置
打开对话框
在可视化模式下通过 Insert 菜单打开:
await page.locator('[aria-label="Insert"]').click(); await page.locator('#rstudio_label_citation_command').click(); await page.locator('[role="dialog"][aria-label="Insert Citation"]') .waitFor({ state: 'visible', timeout: 15000 });选中数据源:点节点行,不点图标
导航树中的每个数据源节点以.pm-navigation-tree-node呈现,内部含有一个img[alt='<Source>'](例如 DataCite、Crossref、PubMed、From DOI)。必须点击节点行本身,而不是图标或其包裹层——点击图片或外层只移动高亮,不会真正选中节点:
const node = page.locator('.pm-navigation-tree-node', { has: page.locator("[alt='DataCite']"), });此外该导航树由react-window 虚拟化:选中后列表会滚动,因此不要对树做快速连点。
高亮 ≠ 选中
.pm-selected-navigation-tree-item高亮类移动到某个源上,不代表该源的搜索结果面板已经激活。验证真正选中必须观察源自身的证据,例如:
- 搜索框的出现(占位符如
Search DataCite for Citations); - 或 Search 按钮变为可见。
绝不能以高亮类作为选中判据。
熬过一次性的初始化重置
这是 Insert Citation 对话框最微妙的时序问题:当对话框的参考文献(bibliography)加载完成时,加载后的配置轮询会触发一次setSelectedPanelProvider,把活动面板弹回对话框初始选中的节点(新开对话框时即 My Sources),同时树的高亮位置保持不变。这个重置最多只触发一次。
因此正确的做法是:选中,若面板被重置回退,则再选一次——第二次点击发生在重置之后,是永久生效的。来自文档的完整防护代码:
const node = page.locator('.pm-navigation-tree-node', { has: page.locator("[alt='DataCite']") }); const box = page.getByPlaceholder('Search DataCite for Citations'); await node.click(); await expect(box).toBeVisible({ timeout: 15000 }); const deadline = Date.now() + 6000; while (Date.now() < deadline) { if (!(await box.isVisible().catch(() => false))) { await node.click(); // reverted; re-select (now permanent) await expect(box).toBeVisible({ timeout: 15000 }); break; } await page.waitForTimeout(200); }这段逻辑在产品代码中已经被封装为 insert_citation.page.ts 的selectSource():它先用expect(...).toPass({ timeout: 30000 })把「点击节点 → 断言搜索框可见」作为一个可重试单元(重置若在面板首次可见前触发,普通可见性等待只会超时),再用一个 6 秒窗口监控面板是否回退并重新点击。R Package 源(typeahead 面板,无 Search 按钮,占位符与 My Sources 共享)则改用「监视该包自身结果行是否消失」来探测重置(见selectPackageSource(),第 127-160 行)。
Latent search:输入不触发搜索,只有 Enter / 粘贴 / Search 按钮才触发
四个网络型引文源(DataCite、Crossref、From DOI、PubMed)使用的都是latent search面板(从 Quarto 编辑器源码确认四个源渲染同一面板,仅树标签与占位符不同)。键入只更新搜索词,不会发起搜索;搜索在以下时机触发:
- 按 Enter;
- 粘贴内容;
- 点击 Search 按钮(
button.pm-insert-citation-panel-latent-search-button)。
Search 按钮只要源面板处于激活态就存在,仅在搜索进行中时禁用。标准流程是「键入 → 点击按钮 → 轮询结果」:
await box.pressSequentially('bobolink'); // real keystrokes, not fill() await page.locator('button.pm-insert-citation-panel-latent-search-button').click(); const results = page.locator('.pm-insert-citation-source-panel-item-detailed'); await expect.poll(() => results.count(), { timeout: 30000 }).toBeGreaterThan(0);务必用pressSequentially,不要用fill():fill()只设置 DOM 值,不会可靠地触发 React 受控输入的onChange,结果就是输入框里明明显示着文字,但内部搜索词仍为空,Enter 或 Search 按钮点击后毫无反应。InsertCitationDialog.search()(insert_citation.page.ts)封装的正是这一序列;R Package 的 typeahead 面板则每键入一个字符就实时过滤,无 Search 按钮,清空残留输入同样使用ControlOrMeta+a+Backspace+pressSequentially的真实按键序列。
引文搜索 RPC 是异步的:浏览器 ↔ rsession 的 RPC 缝
datacite_search、crossref_works、pubmed_search、doi_fetch_csl都是异步 RPC:POST 请求只立即返回{asyncHandle},真正的结果稍后通过/events/get_events以键控于该 handle 的async_completion事件送达。
服务端注册代码在仓库的 panmirror 模块中逐一可见:
- SessionPanmirrorDataCite.cpp:
registerAsyncRpcMethod("datacite_search", dataCiteSearch); - SessionPanmirrorCrossref.cpp:
crossref_works; - SessionPanmirrorPubMed.cpp:
pubmed_search; - SessionPanmirrorDOI.cpp:
doi_fetch_csl。
以 DataCite 为例,dataCiteSearch从请求参数读取query,向 DataCite 的dois端点发起服务端 HTTP 请求,再把 JSON:API 响应转换为记录数组,通过 continuation 异步返回(SessionPanmirrorDataCite.cpp)。
这对测试的直接影响是架构性的:服务端的 HTTP 请求发生在 rsession 进程内,Playwright 无法在 API 边界拦截或模拟它——network.ts 明确注释了这一点(请求源自 C++ session 进程而非浏览器,因此不能用 Playwright 路由 mock)。Playwright 唯一可及的是浏览器 ↔ rsession 的 RPC 缝。所以对这类搜索测试,只能依赖真实服务响应,并通过「跳过不可达服务」来隔离环境问题(详见下一节)。
搜索失败的判定策略:服务错误可跳过,无结果无错误必须失败
引文搜索在rsession中执行,因此 Node 侧的可达性探测通过并不代表查询一定成功——例如 NCBI 会对共享的 CI 出口 IP 限流。这是服务问题,不是产品缺陷,应当跳过(skip)而非失败(fail);但另一方面,超时且既无结果也无错误状态时,仍然必须失败,否则一个静默不渲染任何内容的回归会被掩盖。
citations.test.ts 中凝练的核心模式:
const searchOutcome = async (): Promise<'pending' | 'matched' | 'error'> => { if ((await citation.resultTexts(5)).some((t) => matcher.test(t))) return 'matched'; if (await citation.searchError.isVisible()) return 'error'; return 'pending'; }; await expect.poll(searchOutcome, { timeout: 30000 }).not.toBe('pending'); if ((await searchOutcome()) === 'error') { await citation.cancel(); test.skip(true, 'search failed service-side from this runner'); }其中searchOutcome的三态语义是整套策略的骨架:
- matched:前 5 条渲染结果中有任意一条命中关键词正则(
resultTexts(5)只取当前渲染窗口内的行——列表是 react-window 虚拟化的,该行为在 insert_citation.page.ts 有注释说明); - error:
searchError可见; - pending:两者皆无,继续轮询。
配套的判定工具还有:
- 前置可达性探测:
skipUnlessReachable()基于isServiceReachable()(network.ts)。该探测从 Node 发起、绕过产品本身,任何 HTTP 响应(包括错误状态与重定向)都算可达,只有 DNS、连接、TLS、超时这类网络级失败才算不可达;结果按 URL 缓存在 worker 生命周期内。 searchError的文本过滤:定义于 insert_citation.page.ts,它用正则/error occurred|Unable to search/过滤.pm-insert-citation-source-panel-list-noresults-text节点——必须过滤,因为同一个节点同时承载"No results..."提示和进度文本,直接取节点会把未出错也误判为错误。措辞来源是 panmirror 的errorForStatus()与各面板的 "unknown error" 兜底。- 运行中降级的二次探测:
stageFirstResultOrSkip()(citations.test.ts)在 30 秒内轮询pending,若最终仍无结果且无错误,则调用reprobeService()(network.ts)重新探测——探测通过时保持失败(真回归),探测失败则追溯性跳过(服务在测试中途不可达)。
实战要点小结
把上述模式汇集成一套可复用的检查清单:
- 进可视化模式:统一用
SourcePaneActions.ensureVisualMode(),它自带幂等探测、工具栏就绪等待与首次切换的 pandoc 确认对话框处理;新建文档优先用newMarkdownDoc这个最廉价宿主。 - 选择器作用域:panmirror 的 React UI 在 GWT
[role="dialog"]之外,一律用page级选择器;只有 GWT 按钮(Insert/Cancel)在对话框内。 - 树操作:点击
.pm-navigation-tree-node节点行而非图标;树是 react-window 虚拟化的,禁止连点。 - 选中判据:以源面板自身证据(搜索框占位符、Search 按钮)为准,不要依赖
.pm-selected-navigation-tree-item高亮类。 - 初始化重置:参考文献加载完成后面板会被弹回初始节点(最多一次),采用「可重试的点击+断言单元」加「6 秒回退监控」双保险。
- latent search:键入不触发搜索,用
pressSequentially(非fill())输入后点 Search 按钮,再expect.poll等待结果。 - 异步 RPC:搜索在 rsession 内执行、返回
{asyncHandle}、结果经/events/get_events异步送达,Playwright 无法拦截服务端请求,只能做 Node 侧可达性探测。 - 成败判定:服务端错误 →
cancel()+test.skip(true, ...);超时且无结果无错误 → 保持失败(必要时先reprobeService()区分服务降级与产品回归)。
这套模式不仅服务于引文测试,也同样适用于可视化模式下 Insert/Format 菜单中的其他 React 控件(菜单中的格式化命令、插入操作等),是编写 RStudio panmirror 相关 Playwright 测试时应当首先阅读的行为规范。原始规范文档位于 visual-editor.md,页面对象与测试实现分别见 insert_citation.page.ts 与 citations.test.ts,可作为继续深入研读的入口。
- 开发工具
- 后端
【免费下载链接】rstudio
RStudio is an integrated development environment (IDE) for R
相关推荐
告别繁琐测试!Playwright 富文本编辑器自动化测试指南
告别繁琐测试!Playwright 富文本编辑器自动化测试指南 你还在手动验证富文本编辑器的格式兼容性?还在为不同浏览器显示差异头疼?本文将用Playwrigh
测试开发工具浏览器控制【rstudio】 RStudio项目中的可视化编辑器使用指南
RStudio项目中的可视化编辑器使用指南 概述 RStudio的可视化编辑器为Pandoc Markdown提供了所见即所得的编辑体验。这个编辑器不仅保留了M
开发工具后端CANN/asc-devkit浮点转无符号整型函数
\_\_float2ull\_ru 产品支持情况 <! npu="950" id1 Ascend 950PR/Ascend 950DT:支持 <! end id
人工智能深度学习算子库CANNAscend
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考