168 lines
5.0 KiB
TypeScript
168 lines
5.0 KiB
TypeScript
import { expect, type Page, test } from '@playwright/test'
|
|
import { openCommandPalette, executeCommand } from './helpers'
|
|
|
|
const LIST_ITEM_MARKER = /^[-*+]\s/
|
|
const HEADING_MARKER = /^#{1,6}\s/
|
|
|
|
type MarkdownContent = string
|
|
type MarkdownLine = string
|
|
type LineWindow = {
|
|
line: MarkdownLine
|
|
nextLine: MarkdownLine
|
|
lineAfter: MarkdownLine
|
|
}
|
|
|
|
async function openFirstNoteInBlockEditor(page: Page) {
|
|
const noteListContainer = page.locator('[data-testid="note-list-container"]')
|
|
await noteListContainer.waitFor({ timeout: 5000 })
|
|
await noteListContainer.locator('.cursor-pointer').first().click()
|
|
await page.waitForTimeout(500)
|
|
|
|
const editorContainer = page.locator('.bn-editor')
|
|
await expect(editorContainer).toBeVisible({ timeout: 5000 })
|
|
return editorContainer
|
|
}
|
|
|
|
async function triggerSerialization(page: Page) {
|
|
const editorContainer = await openFirstNoteInBlockEditor(page)
|
|
await editorContainer.click()
|
|
await page.keyboard.press('End')
|
|
await page.keyboard.type(' ')
|
|
await page.waitForTimeout(300)
|
|
}
|
|
|
|
async function toggleRawEditor(page: Page) {
|
|
await openCommandPalette(page)
|
|
await executeCommand(page, 'Toggle Raw')
|
|
await page.waitForTimeout(500)
|
|
}
|
|
|
|
async function readRawEditorContent(page: Page) {
|
|
const rawEditor = page.locator('[data-testid="raw-editor-codemirror"]')
|
|
await expect(rawEditor).toBeVisible({ timeout: 5000 })
|
|
|
|
return page.evaluate(() => {
|
|
const el = document.querySelector('[data-testid="raw-editor-codemirror"]')
|
|
const cm = el?.querySelector('.cm-content')
|
|
return cm?.textContent ?? ''
|
|
})
|
|
}
|
|
|
|
function isBlankLine(line?: MarkdownLine) {
|
|
return line?.trim() === ''
|
|
}
|
|
|
|
function isListItem(line?: MarkdownLine) {
|
|
return LIST_ITEM_MARKER.test(line?.trim() ?? '')
|
|
}
|
|
|
|
function isHeading(line?: MarkdownLine) {
|
|
return HEADING_MARKER.test(line?.trim() ?? '')
|
|
}
|
|
|
|
function hasBlankLineBetweenListItems(window: LineWindow) {
|
|
if (!isListItem(window.line)) {
|
|
return false
|
|
}
|
|
if (!isBlankLine(window.nextLine)) {
|
|
return false
|
|
}
|
|
return isListItem(window.lineAfter)
|
|
}
|
|
|
|
function hasExtraBlankLinesAfterHeading(window: LineWindow) {
|
|
if (!isHeading(window.line)) {
|
|
return false
|
|
}
|
|
if (!isBlankLine(window.nextLine)) {
|
|
return false
|
|
}
|
|
return isBlankLine(window.lineAfter)
|
|
}
|
|
|
|
function findThreeLineViolation(
|
|
rawContent: MarkdownContent,
|
|
hasViolation: (window: LineWindow) => boolean,
|
|
) {
|
|
const lines = rawContent.split('\n')
|
|
for (let i = 0; i < lines.length - 2; i++) {
|
|
if (hasViolation({ line: lines[i], nextLine: lines[i + 1], lineAfter: lines[i + 2] })) {
|
|
return { index: i, lines }
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
function expectNoBlankLinesBetweenListItems(rawContent: MarkdownContent) {
|
|
const violation = findThreeLineViolation(rawContent, hasBlankLineBetweenListItems)
|
|
if (!violation) {
|
|
return
|
|
}
|
|
|
|
const line = violation.lines[violation.index]
|
|
const lineAfter = violation.lines[violation.index + 2]
|
|
throw new Error(
|
|
`Found blank line between list items at line ${violation.index + 1}:\n` +
|
|
` "${line}"\n (blank)\n "${lineAfter}"\n` +
|
|
'This indicates the serializer is adding extra blank lines between list items.',
|
|
)
|
|
}
|
|
|
|
function expectNoExtraBlankLinesAfterHeadings(rawContent: MarkdownContent) {
|
|
const violation = findThreeLineViolation(rawContent, hasExtraBlankLinesAfterHeading)
|
|
if (!violation) {
|
|
return
|
|
}
|
|
|
|
throw new Error(
|
|
`Found multiple blank lines after heading at line ${violation.index + 1}:\n` +
|
|
` "${violation.lines[violation.index]}"\n (blank)\n (blank)\n` +
|
|
'Headings should have at most one blank line after them.',
|
|
)
|
|
}
|
|
|
|
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 }) => {
|
|
await triggerSerialization(page)
|
|
await toggleRawEditor(page)
|
|
const rawContent = await readRawEditorContent(page)
|
|
|
|
// 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()
|
|
expectNoBlankLinesBetweenListItems(rawContent)
|
|
})
|
|
|
|
test('saving without editing does not add whitespace changes', async ({ page }) => {
|
|
await openFirstNoteInBlockEditor(page)
|
|
|
|
await toggleRawEditor(page)
|
|
const contentBefore = await readRawEditorContent(page)
|
|
|
|
await toggleRawEditor(page)
|
|
|
|
// Just click the editor without typing
|
|
await page.locator('.bn-editor').click()
|
|
await page.waitForTimeout(300)
|
|
|
|
await toggleRawEditor(page)
|
|
const contentAfter = await readRawEditorContent(page)
|
|
|
|
expect(contentAfter).toBe(contentBefore)
|
|
})
|
|
|
|
test('headings do not have extra blank lines added after them', async ({ page }) => {
|
|
await triggerSerialization(page)
|
|
await toggleRawEditor(page)
|
|
const rawContent = await readRawEditorContent(page)
|
|
|
|
expectNoExtraBlankLinesAfterHeadings(rawContent)
|
|
})
|
|
})
|