Verifies tight lists stay tight, headings don't gain extra blank lines, and saving without editing doesn't add whitespace changes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
169 lines
6.7 KiB
TypeScript
169 lines
6.7 KiB
TypeScript
import { test, expect } from '@playwright/test'
|
|
import { openCommandPalette, executeCommand } from './helpers'
|
|
|
|
test.describe('BlockNote serializer blank lines fix', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.setViewportSize({ width: 1600, height: 900 })
|
|
await page.goto('/')
|
|
await page.waitForLoadState('networkidle')
|
|
})
|
|
|
|
test('tight lists are serialized without blank lines between items', async ({ page }) => {
|
|
// 1. Open the first note in the note list (mock content has bullet lists)
|
|
const noteListContainer = page.locator('[data-testid="note-list-container"]')
|
|
await noteListContainer.waitFor({ timeout: 5000 })
|
|
const firstNote = noteListContainer.locator('.cursor-pointer').first()
|
|
await firstNote.click()
|
|
await page.waitForTimeout(500)
|
|
|
|
// 2. Wait for the BlockNote editor to render
|
|
const editorContainer = page.locator('.bn-editor')
|
|
await expect(editorContainer).toBeVisible({ timeout: 5000 })
|
|
await page.waitForTimeout(500)
|
|
|
|
// 3. Type a character in the editor to trigger serialization
|
|
await editorContainer.click()
|
|
// Move cursor to end of first paragraph and type a space
|
|
await page.keyboard.press('End')
|
|
await page.keyboard.type(' ')
|
|
await page.waitForTimeout(300)
|
|
|
|
// 4. Toggle to raw editor to see the serialized content
|
|
await openCommandPalette(page)
|
|
await executeCommand(page, 'Toggle Raw')
|
|
await page.waitForTimeout(500)
|
|
|
|
// 5. Read the raw editor content (CodeMirror textarea)
|
|
const rawEditor = page.locator('[data-testid="raw-editor-codemirror"]')
|
|
await expect(rawEditor).toBeVisible({ timeout: 5000 })
|
|
|
|
// Get the raw markdown content via the CodeMirror view
|
|
const rawContent = await page.evaluate(() => {
|
|
const el = document.querySelector('[data-testid="raw-editor-codemirror"]')
|
|
if (!el) return ''
|
|
// CodeMirror stores its content in the view
|
|
const cm = el.querySelector('.cm-content')
|
|
return cm?.textContent ?? ''
|
|
})
|
|
|
|
// 6. Verify: bullet list items should NOT have blank lines between them
|
|
// The mock content has bullet list items like "- First level item"
|
|
// After BlockNote serialization, they should still be tight (no blank lines)
|
|
expect(rawContent).toBeTruthy()
|
|
|
|
// Check there are no patterns like "* item\n\n* item" or "- item\n\n- item"
|
|
// in the serialized output. We look for list markers since the raw editor
|
|
// shows the serialized content.
|
|
const lines = rawContent.split('\n')
|
|
for (let i = 0; i < lines.length - 2; i++) {
|
|
const line = lines[i]
|
|
const nextLine = lines[i + 1]
|
|
const lineAfter = lines[i + 2]
|
|
// If current line is a list item and line after blank is also a list item,
|
|
// that's the bug
|
|
if (/^[-*+]\s/.test(line.trim()) && nextLine?.trim() === '' && /^[-*+]\s/.test(lineAfter?.trim() ?? '')) {
|
|
throw new Error(
|
|
`Found blank line between list items at line ${i + 1}:\n` +
|
|
` "${line}"\n (blank)\n "${lineAfter}"\n` +
|
|
'This indicates the serializer is adding extra blank lines between list items.'
|
|
)
|
|
}
|
|
}
|
|
})
|
|
|
|
test('saving without editing does not add whitespace changes', async ({ page }) => {
|
|
// 1. Open the first note
|
|
const noteListContainer = page.locator('[data-testid="note-list-container"]')
|
|
await noteListContainer.waitFor({ timeout: 5000 })
|
|
const firstNote = noteListContainer.locator('.cursor-pointer').first()
|
|
await firstNote.click()
|
|
await page.waitForTimeout(500)
|
|
|
|
// 2. Wait for BlockNote to load the note
|
|
const editorContainer = page.locator('.bn-editor')
|
|
await expect(editorContainer).toBeVisible({ timeout: 5000 })
|
|
await page.waitForTimeout(500)
|
|
|
|
// 3. Toggle to raw mode to read the note content BEFORE any edits
|
|
await openCommandPalette(page)
|
|
await executeCommand(page, 'Toggle Raw')
|
|
await page.waitForTimeout(500)
|
|
|
|
const rawEditor = page.locator('[data-testid="raw-editor-codemirror"]')
|
|
await expect(rawEditor).toBeVisible({ timeout: 5000 })
|
|
|
|
const contentBefore = await page.evaluate(() => {
|
|
const el = document.querySelector('[data-testid="raw-editor-codemirror"]')
|
|
const cm = el?.querySelector('.cm-content')
|
|
return cm?.textContent ?? ''
|
|
})
|
|
|
|
// 4. Toggle back to WYSIWYG, make no edits, then toggle raw again
|
|
await openCommandPalette(page)
|
|
await executeCommand(page, 'Toggle Raw')
|
|
await page.waitForTimeout(500)
|
|
|
|
// Just click the editor without typing
|
|
await page.locator('.bn-editor').click()
|
|
await page.waitForTimeout(300)
|
|
|
|
// Toggle raw again to compare
|
|
await openCommandPalette(page)
|
|
await executeCommand(page, 'Toggle Raw')
|
|
await page.waitForTimeout(500)
|
|
|
|
const contentAfter = await page.evaluate(() => {
|
|
const el = document.querySelector('[data-testid="raw-editor-codemirror"]')
|
|
const cm = el?.querySelector('.cm-content')
|
|
return cm?.textContent ?? ''
|
|
})
|
|
|
|
// 5. Content should be identical (no extra blank lines added)
|
|
expect(contentAfter).toBe(contentBefore)
|
|
})
|
|
|
|
test('headings do not have extra blank lines added after them', async ({ page }) => {
|
|
// 1. Open the first note
|
|
const noteListContainer = page.locator('[data-testid="note-list-container"]')
|
|
await noteListContainer.waitFor({ timeout: 5000 })
|
|
const firstNote = noteListContainer.locator('.cursor-pointer').first()
|
|
await firstNote.click()
|
|
await page.waitForTimeout(500)
|
|
|
|
const editorContainer = page.locator('.bn-editor')
|
|
await expect(editorContainer).toBeVisible({ timeout: 5000 })
|
|
|
|
// 2. Type a character to trigger serialization
|
|
await editorContainer.click()
|
|
await page.keyboard.press('End')
|
|
await page.keyboard.type(' ')
|
|
await page.waitForTimeout(300)
|
|
|
|
// 3. Toggle raw editor
|
|
await openCommandPalette(page)
|
|
await executeCommand(page, 'Toggle Raw')
|
|
await page.waitForTimeout(500)
|
|
|
|
const rawContent = await page.evaluate(() => {
|
|
const el = document.querySelector('[data-testid="raw-editor-codemirror"]')
|
|
const cm = el?.querySelector('.cm-content')
|
|
return cm?.textContent ?? ''
|
|
})
|
|
|
|
// 4. Verify: no heading is followed by more than one blank line
|
|
const lines = rawContent.split('\n')
|
|
for (let i = 0; i < lines.length - 2; i++) {
|
|
if (/^#{1,6}\s/.test(lines[i].trim())) {
|
|
// A heading followed by two consecutive blank lines is the bug
|
|
if (lines[i + 1]?.trim() === '' && lines[i + 2]?.trim() === '') {
|
|
throw new Error(
|
|
`Found multiple blank lines after heading at line ${i + 1}:\n` +
|
|
` "${lines[i]}"\n (blank)\n (blank)\n` +
|
|
'Headings should have at most one blank line after them.'
|
|
)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
})
|