diff --git a/web/e2e/bug-3113-empty-mermaid-placeholder.spec.ts b/web/e2e/bug-3113-empty-mermaid-placeholder.spec.ts new file mode 100644 index 000000000..47c709d47 --- /dev/null +++ b/web/e2e/bug-3113-empty-mermaid-placeholder.spec.ts @@ -0,0 +1,85 @@ +import { test, expect } from './fixtures'; +import { browserLogin, EDITOR_SELECTOR, SYNCED_BADGE_SELECTOR } from './lib/collab-helpers'; + +/** + * BUG-3113: an empty mermaid block showed "Rendering..." forever. + * + * The mermaid NodeView seeded its diagram element with a "Rendering..." + * placeholder and queued a render only when the source was non-empty, so a + * block that was EMPTY WHEN ITS VIEW WAS BUILT kept the placeholder with + * nothing coming to replace it. Both ways a block starts empty are covered — + * loading an item that already holds one, and typing the fence — per the + * contract on BUG-3113. + * + * The control leg matters (CONVE-12): "the placeholder is absent" would also + * hold if mermaid rendering were broken outright and the diagram element were + * left empty for every block. So the same item carries a block WITH source, + * which must render an SVG, and the empty-block assertions are only reached + * once that has happened. + */ + +const DIAGRAM = '.mermaid-wrapper .mermaid-diagram'; + +async function openDoc( + page: import('@playwright/test').Page, + fixture: import('./fixtures').SuiteFixture, + request: import('@playwright/test').APIRequestContext, + content: string, +) { + const ws = fixture.workspaceSlug; + const resp = await request.post(`/api/v1/workspaces/${ws}/collections/docs/items`, { + headers: { Authorization: `Bearer ${fixture.apiToken}`, 'Content-Type': 'application/json' }, + data: { title: `BUG-3113 ${Date.now()}`, fields: '{}', content }, + }); + expect(resp.ok()).toBe(true); + const { slug } = (await resp.json()) as { slug: string }; + await browserLogin(page); + await page.goto(`/${fixture.adminUsername}/${ws}/docs/${slug}`); + await expect(page.locator(EDITOR_SELECTOR)).toBeVisible(); + await expect(page.locator(SYNCED_BADGE_SELECTOR)).toBeVisible(); +} + +test.describe('BUG-3113: an empty mermaid block is not stuck on the placeholder', () => { + test.beforeEach(({}, testInfo) => { + test.skip(testInfo.project.name !== 'desktop-chromium', 'editor rendering; one viewport is enough'); + }); + + test('loading an item that already holds an empty mermaid block', async ({ page, fixture, request }) => { + await openDoc( + page, + fixture, + request, + 'Control:\n\n```mermaid\ngraph TD; A-->B\n```\n\nEmpty:\n\n```mermaid\n```\n\nAfter.\n', + ); + const diagrams = page.locator(`${EDITOR_SELECTOR} ${DIAGRAM}`); + await expect(diagrams).toHaveCount(2); + + // CONTROL: the block with source renders, so rendering works here. + await expect(diagrams.nth(0).locator('svg')).toBeVisible({ timeout: 15_000 }); + + // THE ASSERTION: the empty block shows nothing, not a placeholder that + // claims work is in progress. + await expect(diagrams.nth(1)).not.toContainText('Rendering'); + await expect(diagrams.nth(1)).toHaveText(''); + }); + + test('typing the fence into the editor', async ({ page, fixture, request }) => { + await openDoc(page, fixture, request, 'Control:\n\n```mermaid\ngraph TD; A-->B\n```\n\nType below.\n'); + const diagrams = page.locator(`${EDITOR_SELECTOR} ${DIAGRAM}`); + await expect(diagrams).toHaveCount(1); + await expect(diagrams.nth(0).locator('svg')).toBeVisible({ timeout: 15_000 }); + + // Put the caret at the end of the last paragraph, start a new one, and + // type the fence the way a user starts a diagram. + await page.locator(`${EDITOR_SELECTOR} p`, { hasText: 'Type below.' }).click(); + await page.keyboard.press('End'); + await page.keyboard.press('Enter'); + await page.keyboard.type('```mermaid'); + await page.keyboard.press('Enter'); + + // Precondition: the fence really produced a SECOND mermaid block. + await expect(diagrams).toHaveCount(2); + await expect(diagrams.nth(1)).not.toContainText('Rendering'); + await expect(diagrams.nth(1)).toHaveText(''); + }); +}); diff --git a/web/src/lib/components/editor/Editor.svelte b/web/src/lib/components/editor/Editor.svelte index 23ec1dc3c..26c4e5574 100644 --- a/web/src/lib/components/editor/Editor.svelte +++ b/web/src/lib/components/editor/Editor.svelte @@ -558,7 +558,6 @@ const diagram = document.createElement('div'); diagram.className = 'mermaid-diagram'; diagram.setAttribute('contenteditable', 'false'); - diagram.textContent = 'Rendering...'; const pre = document.createElement('pre'); pre.classList.add('code-block', 'mermaid-source'); @@ -587,7 +586,15 @@ // which sees only the DOM. Set even when empty, so the theme // path can tell "no source" from "not a diagram we know". mermaidSources.set(diagram, source); + // At creation the placeholder is shown only alongside the + // render queued to replace it. An empty block — a freshly + // typed fence, or one loaded empty — has nothing coming, so + // it stays empty, which is also what update() leaves when a + // diagram's source is cleared (BUG-3113). Later renders + // (update(), the theme re-render) show no placeholder: the + // previous diagram stays up until the new one lands. if (source) { + diagram.textContent = 'Rendering...'; queueMermaidRender(source, diagram); }