Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,8 @@
* "Adding different instances of a keyed plugin (suggestion$)".
*
* 2. A markdown file authored outside the editor (e.g. the former Monaco editor) is rarely in the
* editor's canonical serialization. On open, a deferred view-plugin transaction re-serializes the
* doc to canonical markdown and emits one update — which, compared against the raw saved bytes,
* falsely marks the file dirty ("unsaved changes"). The fix normalizes the dirty-check baseline to
* the canonical form; this asserts that normalized form equals what the live editor emits.
* editor's canonical serialization. The dirty-check baseline must use that canonical form so any
* mount-time update remains clean.
*/

import { sleep } from '@sim/utils/helpers'
Expand Down Expand Up @@ -79,28 +77,30 @@ describe('normalizeMarkdownContent — dirty-on-open baseline', () => {
})
})

describe('baseline neutralizes the mount-time dirty signal', () => {
it('the editor mount serialization equals the normalized baseline (so isDirty stays false)', async () => {
describe('baseline neutralizes mount-time dirty signals', () => {
it('mounting never produces content that differs from the normalized baseline', async () => {
const raw = '# H\n\n* bullet\n\n| a | b |\n| --- | --- |\n| 1 | 2 |\n\n> quote\n'
const { frontmatter, body } = splitFrontmatter(raw)
const canonical = normalizeMarkdownContent(raw)
host = document.createElement('div')
document.body.appendChild(host)

let emitted: string | null = null
let dirtyUpdate: string | null = null
editor = new Editor({
element: host,
extensions: createMarkdownEditorExtensions({ placeholder: 'x' }),
content: parseMarkdownToDoc(body),
onUpdate: ({ editor }) => {
emitted = applyFrontmatter(frontmatter, postProcessSerializedMarkdown(editor.getMarkdown()))
const content = applyFrontmatter(
frontmatter,
postProcessSerializedMarkdown(editor.getMarkdown())
)
if (content !== canonical) dirtyUpdate = content
},
})

await sleep(30)

// The deferred mount transaction re-serializes to canonical markdown; the baseline must match it
// exactly, so `content === savedContent` and the file is never falsely dirty on open.
expect(emitted).not.toBeNull()
expect(emitted).toBe(normalizeMarkdownContent(raw))
expect(dirtyUpdate).toBeNull()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,17 @@
* back to markdown.
*/

import { act, createElement } from 'react'
import { sleep } from '@sim/utils/helpers'
import { Editor } from '@tiptap/core'
import { EditorContent } from '@tiptap/react'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { createMarkdownEditorExtensions } from './editor-extensions'

let editor: Editor | null = null
let root: Root | null = null
let host: HTMLElement | null = null

beforeEach(() => {
// The live extension set's placeholder viewport-tracking and suggestion popups use these; jsdom
Expand All @@ -32,16 +37,25 @@ beforeEach(() => {
})

afterEach(() => {
if (root) act(() => root?.unmount())
editor?.destroy()
editor = null
root = null
host?.remove()
host = null
})

function mount(markdown: string): Editor {
return new Editor({
const mountedEditor = new Editor({
extensions: createMarkdownEditorExtensions({ placeholder: '' }),
content: markdown,
contentType: 'markdown',
})
host = document.createElement('div')
document.body.appendChild(host)
root = createRoot(host)
act(() => root?.render(createElement(EditorContent, { editor: mountedEditor })))
return mountedEditor
}

function posOf(ed: Editor, typeName: string): number {
Expand All @@ -54,16 +68,11 @@ function posOf(ed: Editor, typeName: string): number {

/** React node views flush on a microtask after mount, so DOM assertions need one tick. */
function nextTick(): Promise<void> {
return sleep(0)
return act(async () => {
await sleep(0)
})
}

// The hover "Raw HTML"/"Footnote" badge is rendered by `RawBlockView` through
// `ReactNodeViewRenderer`, which only flushes its React portal once `@tiptap/react`'s
// `contentComponent` is set — that requires mounting through `<EditorContent>` (a real React render
// tree), which this repo's tests don't do for this directory (no `@testing-library/react` installed
// here) and constructing a plain `new Editor()` doesn't provide. What IS verifiable and matters more
// at this level — the node renders with the correct wrapper class and holds the exact raw source
// text — is covered below; the badge itself is decorative chrome, checked manually.
describe('raw markdown snippet node views (live editor)', () => {
it('renders a raw HTML block with the correct wrapper class and exact raw source', async () => {
editor = mount('<details><summary>More</summary>\n\nbody\n\n</details>')
Expand Down
30 changes: 15 additions & 15 deletions apps/sim/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,21 +129,21 @@
"@t3-oss/env-nextjs": "0.13.4",
"@tanstack/react-query": "5.90.8",
"@tanstack/react-virtual": "3.13.24",
"@tiptap/core": "3.26.1",
"@tiptap/extension-code": "3.26.1",
"@tiptap/extension-code-block": "3.26.1",
"@tiptap/extension-collaboration": "3.26.1",
"@tiptap/extension-collaboration-caret": "3.26.1",
"@tiptap/extension-image": "3.26.1",
"@tiptap/extension-list": "3.26.1",
"@tiptap/extension-placeholder": "3.26.1",
"@tiptap/extension-paragraph": "3.26.1",
"@tiptap/extension-table": "3.26.1",
"@tiptap/markdown": "3.26.1",
"@tiptap/pm": "3.26.1",
"@tiptap/react": "3.26.1",
"@tiptap/starter-kit": "3.26.1",
"@tiptap/suggestion": "3.26.1",
"@tiptap/core": "3.30.5",
"@tiptap/extension-code": "3.30.5",
"@tiptap/extension-code-block": "3.30.5",
"@tiptap/extension-collaboration": "3.30.5",
"@tiptap/extension-collaboration-caret": "3.30.5",
"@tiptap/extension-image": "3.30.5",
"@tiptap/extension-list": "3.30.5",
"@tiptap/extension-placeholder": "3.30.5",
"@tiptap/extension-paragraph": "3.30.5",
"@tiptap/extension-table": "3.30.5",
"@tiptap/markdown": "3.30.5",
"@tiptap/pm": "3.30.5",
"@tiptap/react": "3.30.5",
"@tiptap/starter-kit": "3.30.5",
"@tiptap/suggestion": "3.30.5",
"@tiptap/y-tiptap": "3.0.7",
"@trigger.dev/core": "4.5.12",
"@trigger.dev/sdk": "4.5.12",
Expand Down
Loading
Loading