Преглед изворни кода

style(ui): simplify Mermaid preview controls

fz пре 2 недеља
родитељ
комит
fc0c885287

+ 2 - 2
.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.md
-2026-09-07-web-mermaid-preview.md: c891b4133e48a555de945c250679555f91f44c2f
-2026-09-07-web-mermaid-preview.zh.md: a4c79c430a07b2655c85354a4406da3914bbd4c4
+2026-09-07-web-mermaid-preview.md: df50e7c7f8b78a869d7d5b761864c06648f8d064
+2026-09-07-web-mermaid-preview.zh.md: 282e6fb4365a56425f2b341e2367e4a2cdae5c1c

+ 3 - 1
.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.md

@@ -10,7 +10,7 @@ Assistant replies can describe diagrams in Mermaid code fences, but readers must
 
 ## Decision
 
-Chat enables settled fence previews through `MarkdownLabels.mermaid`. The shared Markdown renderer uses the parsed fence language; streaming fences and consumers without those labels retain code display. The static [UI primitives package](../../../../packages/client/ui-primitives/README.md) owns `MermaidPreview`, which accepts source and localized labels without Session, file, or Cordis dependencies. `CodeBlock.preview` reuses the banner and copy action; copying always reads the source prop.
+Chat enables settled fence previews through `MarkdownLabels.mermaid`. The shared Markdown renderer uses the parsed fence language; streaming fences and consumers without those labels retain code display. The static [UI primitives package](../../../../packages/client/ui-primitives/README.md) owns `MermaidPreview`, which accepts source and localized labels without Session, file, or Cordis dependencies. `CodeBlock.preview` owns view switching and source copying. Previews omit the language banner and expose compact icon actions on hover or keyboard focus; touch devices keep those actions visible. Source and preview share the same focused toggle button, and copying always reads the source prop.
 
 Mermaid loads on demand. Its public render API serializes diagram work, and each call removes its temporary measurement DOM in `finally`. Strict security, disabled HTML labels, the neutral theme, and error-rendering policy cannot be overridden by diagram configuration. Generated SVG is displayed as an image without installing diagram links or scripts. Intrinsic dimensions come from the SVG viewBox; large diagrams shrink to fit, and the canvas stays light in both application themes.
 
@@ -18,6 +18,8 @@ Source replacement and unmounting cancel result publication. Cancellation before
 
 ## Alternatives considered
 
+**Keep the code banner above diagrams.** A language label and permanent text controls distract from the diagram. Overlay actions retain source access without reserving a title row.
+
 **Render each streamed chunk.** Incomplete diagrams are frequently invalid, and repeated layout work competes with text streaming. The existing message-settlement boundary provides a complete source value.
 
 **Put rendering inside Chat or add a general preview registry.** A shared primitive with plain props satisfies reuse without another registry or feature-plugin dependency. This follows the [shared-control rule](../architecture/2026-09-05-shared-client-control-primitives.md).

+ 3 - 1
.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.zh.md

@@ -10,7 +10,7 @@ Assistant 回复可以在 Mermaid 代码 fence 中描述图表,但读者必须
 
 ## 决策
 
-Chat 通过 `MarkdownLabels.mermaid` 启用已定稿 fence 预览。共享 Markdown 渲染器使用解析后的 fence 语言;流式 fence 和未传入这些 label 的调用方保留代码显示。静态 [UI primitives 包](../../../../packages/client/ui-primitives/README.zh.md)拥有 `MermaidPreview`,它接收源码与本地化 label,不依赖 Session、文件或 Cordis。`CodeBlock.preview` 复用标题栏和复制操作;复制始终读取源码 prop。
+Chat 通过 `MarkdownLabels.mermaid` 启用已定稿 fence 预览。共享 Markdown 渲染器使用解析后的 fence 语言;流式 fence 和未传入这些 label 的调用方保留代码显示。静态 [UI primitives 包](../../../../packages/client/ui-primitives/README.zh.md)拥有 `MermaidPreview`,它接收源码与本地化 label,不依赖 Session、文件或 Cordis。`CodeBlock.preview` 拥有视图切换与源码复制。预览省略语言标题栏,悬停或键盘聚焦时显示紧凑的图标操作;触屏上保持操作可见。源码与预览使用同一个已聚焦的切换按钮,复制始终读取源码 prop。
 
 Mermaid 按需加载。它的公开 render API 串行执行图表工作,每次调用在 `finally` 中移除临时测量 DOM。图表配置无法覆盖严格安全模式、禁用 HTML label、中性主题和错误渲染策略。生成的 SVG 以图片显示,不安装图内链接或脚本。固有尺寸取自 SVG viewBox;大图缩小以适应宽度,画布在两种应用主题下均保持浅色。
 
@@ -18,6 +18,8 @@ Mermaid 按需加载。它的公开 render API 串行执行图表工作,每次
 
 ## 考虑过的替代方案
 
+**在图表上方保留代码标题栏。** 语言标记和常驻的文字操作分散了对图表的注意力。浮层操作无需占用标题行即可保留源码入口。
+
 **渲染每个流式分片。** 不完整的图表经常无效,反复布局也会与文本流式输出竞争。已有的消息定稿边界提供完整源码值。
 
 **把渲染放在 Chat 内或增加通用预览注册表。** 接收普通 prop 的共享原语无需另一套注册表或功能插件依赖即可满足复用。这遵循[共享控件规则](../architecture/2026-09-05-shared-client-control-primitives.zh.md)。

+ 3 - 4
apps/web/tests/expected/markdown-mermaid/ui.expected.md

@@ -4,6 +4,8 @@
   - button "Session log":
     - text: Session log
     - img
+  - button "Open the sidebar":
+    - img
   - tablist:
     - tab "Chat" [selected]
     - tab "Trajectory"
@@ -11,20 +13,17 @@
 - button "Copy":
   - img
 - heading "Mermaid previews" [level=1]
-- text: mermaid
 - button "Source"
 - button "Copy"
+- tooltip "Copy"
 - img "Mermaid diagram"
-- text: mermaid
 - button "Source"
 - button "Copy"
 - img "Mermaid diagram"
-- text: mermaid
 - button "Source"
 - button "Copy"
 - status: Unable to render this diagram. The source is shown below.
 - code: flowchart LR A[unfinished
-- text: mermaid
 - button "Source"
 - button "Copy"
 - img "Mermaid diagram"

+ 2 - 4
apps/web/tests/expected/markdown-mermaid/zh.expected.md

@@ -4,6 +4,8 @@
   - button "Session 日志":
     - text: Session 日志
     - img
+  - button "展开侧栏":
+    - img
   - tablist:
     - tab "对话" [selected]
     - tab "轨迹"
@@ -11,20 +13,16 @@
 - button "复制":
   - img
 - heading "Mermaid previews" [level=1]
-- text: mermaid
 - button "源码"
 - button "复制"
 - img "Mermaid 图表"
-- text: mermaid
 - button "源码"
 - button "复制"
 - img "Mermaid 图表"
-- text: mermaid
 - button "源码"
 - button "复制"
 - status: 无法渲染此图表,源码如下。
 - code: flowchart LR A[unfinished
-- text: mermaid
 - button "源码"
 - button "复制"
 - img "Mermaid 图表"

+ 23 - 2
apps/web/tests/markdown-mermaid.e2e.ts

@@ -94,9 +94,20 @@ describe('web e2e: Mermaid chat previews', () => {
     expect(await page.locator('pre code').allTextContents()).toContain(INVALID)
     expect(await images.first().evaluate(node => decodeURIComponent((node as HTMLImageElement).src))).toContain('共享渲染器')
     const first = page.locator('.md-code-block').first()
+    const controls = first.locator('[class*="bannerWrap"]')
+    expect(await first.getByText('mermaid', { exact: true }).count()).toBe(0)
+    await page.mouse.move(0, 0)
+    expect(await controls.evaluate(node => getComputedStyle(node).opacity)).toBe('0')
+    await first.hover()
+    expect(await controls.evaluate(node => getComputedStyle(node).opacity)).toBe('1')
     await first.getByRole('button', { name: 'Copy', exact: true }).click()
     expect(await page.evaluate(() => navigator.clipboard.readText())).toBe(FLOW)
-    await first.getByRole('button', { name: 'Source', exact: true }).click()
+    await page.mouse.move(0, 0)
+    await first.getByRole('button', { name: 'Source', exact: true }).focus()
+    expect(await controls.evaluate(node => getComputedStyle(node).opacity)).toBe('1')
+    await page.keyboard.press('Enter')
+    expect(await first.getByRole('button', { name: 'Preview', exact: true })
+      .evaluate(node => node === document.activeElement)).toBe(true)
     expect(await first.locator('pre code').textContent()).toBe(FLOW)
     await first.getByRole('button', { name: 'Preview', exact: true }).click()
     await first.getByRole('img').waitFor()
@@ -106,6 +117,8 @@ describe('web e2e: Mermaid chat previews', () => {
     expect(dialogs).toEqual([])
     expect(tripwire.pageErrors).toEqual([])
     expect(tripwire.warnings).toEqual([])
+    await page.getByRole('heading', { name: 'Mermaid previews' }).click()
+    await page.mouse.move(0, 0)
     const snapshot = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd))
       .split(SEED_ID).join('{{seededId}}')
     await compareOrRefreshGolden(join(SNAPSHOT_DIR, 'ui.expected.md'), snapshot, MODE)
@@ -113,11 +126,19 @@ describe('web e2e: Mermaid chat previews', () => {
   }, 60_000)
 
   it.skipIf(MODE === 'record')('localizes the preview and failure states in Chinese', async () => {
-    const page = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: 'zh-CN', timezoneId: 'Asia/Shanghai' })
+    const page = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: 'zh-CN', timezoneId: 'Asia/Shanghai', hasTouch: true })
     await openConversation(page, scaffold)
     await expect.poll(() => page.getByRole('img', { name: 'Mermaid 图表' }).count(), { timeout: 20_000 }).toBe(3)
     expect(await page.getByRole('button', { name: '源码', exact: true }).count()).toBe(4)
+    const controls = page.locator('.md-code-block').first().locator('[class*="bannerWrap"]')
+    expect(await controls.evaluate(node => getComputedStyle(node).opacity)).toBe('1')
+    expect(await page.getByRole('button', { name: '源码', exact: true }).first()
+      .evaluate(node => node.getBoundingClientRect().width)).toBe(44)
     expect(await page.getByText('无法渲染此图表,源码如下。', { exact: true }).count()).toBe(1)
+    const firstImage = page.getByRole('img', { name: 'Mermaid 图表' }).first()
+    await expect.poll(() => firstImage.evaluate(node => (node as HTMLImageElement).naturalWidth)).toBeGreaterThan(0)
+    expect(await controls.evaluate(node => node.getBoundingClientRect().top))
+      .toBeGreaterThanOrEqual(await firstImage.evaluate(node => node.getBoundingClientRect().bottom))
     const snapshot = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd))
       .split(SEED_ID).join('{{seededId}}')
     await compareOrRefreshGolden(join(SNAPSHOT_DIR, 'zh.expected.md'), snapshot, MODE)

+ 2 - 2
packages/client/ui-chat/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/client/ui-chat/README.md
-README.md: 6115e18f303cfa6ecc63379eabf8a7ad6f56b121
-README.zh.md: 3cb069cdf6cedb6d0c5c045f95e106c2c26959b6
+README.md: 951f88e5fa61a27802fb7df11a3347ba9676a66c
+README.zh.md: 3a180afb43c4262d6054e1f19deefa490b9b36de

+ 1 - 1
packages/client/ui-chat/README.md

@@ -33,7 +33,7 @@ Each nonempty appended `system/message` owns a collapsed prompt row, including a
 <a id="mermaid-previews"></a>
 ## Mermaid previews
 
-Settled Assistant Markdown shows `mermaid` code fences as diagrams. Each fence offers Source/Preview switching and copies the original Mermaid source in either view. Streaming fences remain code until the message settles; invalid diagrams show an error and their source without disrupting other blocks. Chat enables the shared [Mermaid preview primitive](../ui-primitives/README.md) through localized labels. This is a presentation change and adds no model prompt, tool, or Session event.
+Settled Assistant Markdown shows `mermaid` code fences as diagrams. Diagrams have no language banner. Source/Preview and Copy icon actions appear on hover or keyboard focus and remain visible on touch devices; both views copy the original Mermaid source. Streaming fences remain code until the message settles; invalid diagrams show an error and their source without disrupting other blocks. Chat enables the shared [Mermaid preview primitive](../ui-primitives/README.md) through localized labels. This is a presentation change and adds no model prompt, tool, or Session event.
 
 -----
 

+ 1 - 1
packages/client/ui-chat/README.zh.md

@@ -35,7 +35,7 @@ kind: "package-reference"
 <a id="mermaid-previews"></a>
 ## Mermaid 预览
 
-已定稿的 Assistant Markdown 把 `mermaid` 代码 fence 显示为图表。每个 fence 支持源码/预览切换,在任一视图中复制的都是原始 Mermaid 源码。流式 fence 在消息定稿前保持代码显示;非法图表显示错误与源码,不影响其他块。Chat 通过本地化 label 启用共享的 [Mermaid 预览原语](../ui-primitives/README.zh.md)。这项展示变更不增加模型提示词、工具或 Session 事件。
+已定稿的 Assistant Markdown 把 `mermaid` 代码 fence 显示为图表。图表不显示语言标题栏。源码/预览切换与复制图标在悬停或键盘聚焦时出现,触屏上保持可见;两种视图复制的都是原始 Mermaid 源码。流式 fence 在消息定稿前保持代码显示;非法图表显示错误与源码,不影响其他块。Chat 通过本地化 label 启用共享的 [Mermaid 预览原语](../ui-primitives/README.zh.md)。这项展示变更不增加模型提示词、工具或 Session 事件。
 
 -----
 

+ 2 - 2
packages/client/ui-primitives/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/client/ui-primitives/README.md
-README.md: 1e9ee01f6c2243c8b980e45cf512dda1af330af0
-README.zh.md: 02c0417c359070c2f4a4ca11a22709c401847cec
+README.md: f2694588e2ce96f10055802adaf5ff23b9cae9f1
+README.zh.md: cbd7fec0c1708b9d902dff2302c9472ed2bbb56b

+ 1 - 1
packages/client/ui-primitives/README.md

@@ -76,7 +76,7 @@ The catalog above lists what each export is for; this section covers the behavio
 
 `MermaidPreview` renders complete Mermaid source on a light canvas. Rendering loads Mermaid on demand, uses strict security, and exposes the generated SVG as an image with no diagram link handlers. A render failure shows the original source with the supplied error label; replacing the source discards late results from the previous render. The diagram keeps its intrinsic size and shrinks to fit the available width.
 
-Supply `MarkdownLabels.mermaid` to enable `mermaid` fence previews in a Markdown consumer; without it, fences remain code. Previews start after the message settles. `CodeBlock.preview` supplies an optional alternate body and localized Source/Preview switch labels while the existing Copy button always copies the source. [The Mermaid decision](../../../.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.md) records the rendering and reuse choices.
+Supply `MarkdownLabels.mermaid` to enable `mermaid` fence previews in a Markdown consumer; without it, fences remain code. Previews start after the message settles. `CodeBlock.preview` supplies an optional headerless body with icon actions for source switching and copying. Actions appear on hover or keyboard focus, and stay visible on touch devices; copying always retains the source. [The Mermaid decision](../../../.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.md) records the rendering and reuse choices.
 
 ### Localizing copy
 

+ 1 - 1
packages/client/ui-primitives/README.zh.md

@@ -76,7 +76,7 @@ kind: "package-library"
 
 `MermaidPreview` 在浅色画布上渲染完整的 Mermaid 源码。渲染按需加载 Mermaid、使用严格安全模式,并把生成的 SVG 显示为图片,不绑定图内链接处理器。渲染失败时显示原始源码与传入的错误文案;替换源码后会丢弃前一次渲染的延迟结果。图表保留固有尺寸,并在可用宽度不足时缩小。
 
-为 Markdown 调用方传入 `MarkdownLabels.mermaid` 即可启用 `mermaid` fence 预览;未传入时,fence 保持代码显示。预览在消息定稿后开始。`CodeBlock.preview` 提供可选的替代正文和本地化的「源码/预览」切换文案,现有「复制」按钮始终复制源码。[Mermaid 决策](../../../.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.zh.md)记录了渲染与复用选择。
+为 Markdown 调用方传入 `MarkdownLabels.mermaid` 即可启用 `mermaid` fence 预览;未传入时,fence 保持代码显示。预览在消息定稿后开始。`CodeBlock.preview` 提供无标题栏的替代正文,以及切换源码和复制的图标操作。操作在悬停或键盘聚焦时出现,触屏上保持可见;复制始终保留源码。[Mermaid 决策](../../../.agents/notes/implemented/feature/2026-09-07-web-mermaid-preview.zh.md)记录了渲染与复用选择。
 
 ### 本地化文案
 

+ 81 - 0
packages/client/ui-primitives/src/markdown/CodeBlock.module.css

@@ -122,3 +122,84 @@
   content: counter(source-line);
   user-select: none;
 }
+
+/* Alternate content is a figure; its actions overlay instead of taking a title row. */
+.preview.showingPreview {
+  background: transparent;
+}
+
+.preview .bannerWrap {
+  position: absolute;
+  top: 8px;
+  right: 8px;
+  z-index: 1;
+  background: transparent;
+}
+
+.preview .banner {
+  padding: 2px;
+  background: var(--dsw-alias-bg-base);
+  border: 0;
+  border-radius: 8px;
+  box-shadow: var(--dsw-elevation-panel);
+}
+
+.preview .action {
+  gap: 2px;
+}
+
+.iconButton {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  width: 32px;
+  height: 32px;
+  border-radius: 6px;
+  color: var(--dsw-alias-label-secondary);
+}
+
+.iconButton span {
+  display: flex;
+}
+
+.iconButton:hover {
+  color: var(--dsw-alias-label-primary);
+  background: var(--dsw-alias-interactive-bg-hover);
+}
+
+.preview:not(.showingPreview) :where(pre) {
+  padding-top: 48px;
+}
+
+@media (hover: hover) and (pointer: fine) {
+  .preview .bannerWrap {
+    opacity: 0;
+    pointer-events: none;
+  }
+
+  .preview:hover .bannerWrap,
+  .preview:focus-within .bannerWrap {
+    opacity: 1;
+    pointer-events: auto;
+  }
+}
+
+@media (pointer: coarse) {
+  .preview {
+    padding-bottom: 56px;
+  }
+
+  .preview .bannerWrap {
+    top: auto;
+    bottom: 8px;
+  }
+
+  .preview .iconButton {
+    width: 44px;
+    height: 44px;
+  }
+
+  .preview:not(.showingPreview) :where(pre) {
+    padding-top: 16px;
+  }
+}

+ 32 - 9
packages/client/ui-primitives/src/markdown/CodeBlock.tsx

@@ -2,6 +2,8 @@ import { Fragment, useCallback, useMemo, useRef, useState, useSyncExternalStore
 import type { CSSProperties, ReactNode } from 'react'
 import clsx from 'clsx'
 import { writeClipboard } from '../clipboard.ts'
+import { Tooltip } from '../Tooltip.tsx'
+import { IconCheckOutline16, IconCodeOutline16, IconCopyOutline16 } from '../icons/index.tsx'
 import {
   StreamingHighlightSession, grammarLoadCount, highlightToHtml, subscribeGrammarLoaded,
 } from './highlight.ts'
@@ -31,7 +33,7 @@ export interface CodeBlockProps {
   copyLabel: string
   /** Copy-button label during the post-copy confirmation window. */
   copiedLabel: string
-  /** Optional settled preview with localized switch labels; copying always retains the source text. */
+  /** Headerless settled preview with hover/focus actions; copying always retains the source text. */
   preview?: { content: ReactNode; previewLabel: string; sourceLabel: string } | undefined
 }
 
@@ -173,24 +175,45 @@ export function CodeBlock({ code, lang, streaming, className, lineNumbers = fals
         <div dangerouslySetInnerHTML={{ __html: html }} />
       )
 
+  const copyActionLabel = copied ? copiedLabel : copyLabel
+  const copyAction = (
+    <button
+      type="button"
+      className={clsx(css.copyButton, previewAvailable && css.iconButton)}
+      aria-label={previewAvailable ? copyActionLabel : undefined}
+      onClick={onCopy}
+    >
+      {previewAvailable
+        ? <span aria-hidden="true">{copied ? <IconCheckOutline16 /> : <IconCopyOutline16 />}</span>
+        : copyActionLabel}
+    </button>
+  )
+
   return (
-    <div ref={rootRef} className={clsx(css.block, 'md-code-block', lineNumbers && css.numbered, className)}
+    <div ref={rootRef} className={clsx(css.block, 'md-code-block', lineNumbers && css.numbered, previewAvailable && css.preview, showingPreview && css.showingPreview, className)}
       data-line-numbers={lineNumbers || undefined}
       style={sourceLines === undefined ? undefined : {
         '--dsl-code-block-line-number-width': `${Math.max(2, String(sourceLines.length).length)}ch`,
       } as CSSProperties}>
       <div className={css.bannerWrap}>
         <div className={css.banner}>
-          <div className={css.infostring}>{lang ?? ''}</div>
+          {!previewAvailable && <div className={css.infostring}>{lang ?? ''}</div>}
           <div className={css.action}>
             {previewAvailable && (
-              <button type="button" className={css.copyButton} onClick={() => { setShowSource(!showSource) }}>
-                {showingPreview ? preview.sourceLabel : preview.previewLabel}
-              </button>
+              <Tooltip label={showingPreview ? preview.sourceLabel : preview.previewLabel} side="top">
+                <button
+                  type="button"
+                  className={clsx(css.copyButton, css.iconButton)}
+                  aria-label={showingPreview ? preview.sourceLabel : preview.previewLabel}
+                  onClick={() => { setShowSource(!showSource) }}
+                >
+                  <span aria-hidden="true"><IconCodeOutline16 /></span>
+                </button>
+              </Tooltip>
             )}
-            <button type="button" className={css.copyButton} onClick={onCopy}>
-              {copied ? copiedLabel : copyLabel}
-            </button>
+            {previewAvailable
+              ? <Tooltip label={copyActionLabel} side="top">{copyAction}</Tooltip>
+              : copyAction}
           </div>
         </div>
       </div>

+ 1 - 1
packages/client/ui-primitives/src/markdown/MermaidPreview.module.css

@@ -2,7 +2,7 @@
   overflow: auto;
   padding: 16px;
   background: var(--dsw-static-neutral-00);
-  border-radius: 0 0 12px 12px;
+  border-radius: 12px;
 }
 
 .diagram {

+ 4 - 0
packages/client/ui-primitives/tests/mermaid-preview.client.spec.tsx

@@ -87,12 +87,16 @@ describe('Markdown Mermaid fences', () => {
     expect(screen.queryByRole('button', { name: labels.source })).toBeNull()
     view.rerender(<MarkdownText {...props} />)
     await screen.findByRole('img', { name: labels.diagram })
+    expect(screen.queryByText('mermaid')).toBeNull()
+    expect(screen.getByRole('button', { name: labels.source }).textContent).toBe('')
+    expect(screen.getByRole('button', { name: markdownLabels.code.copyLabel }).textContent).toBe('')
     fireEvent.click(screen.getByRole('button', { name: markdownLabels.code.copyLabel }))
     await screen.findByRole('button', { name: markdownLabels.code.copiedLabel })
     expect(writeText).toHaveBeenCalledWith(source)
     fireEvent.click(screen.getByRole('button', { name: labels.source }))
     expect(view.container.querySelector('pre code')?.textContent).toBe(source)
     expect(screen.queryByRole('img')).toBeNull()
+    expect(screen.queryByText('mermaid')).toBeNull()
     fireEvent.click(screen.getByRole('button', { name: labels.preview }))
     await screen.findByRole('img', { name: labels.diagram })
   })