hololake-platform/tests/smoke/blocknote-serializer-blank-lines.spec.ts

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)
})
})