Files
tolaria/tests/smoke/new-note-heading-paste-regression.spec.ts
2026-05-03 18:27:26 +02:00

163 lines
5.4 KiB
TypeScript

import fs from 'fs'
import { test, expect, type Page } from '@playwright/test'
import { createFixtureVaultCopy, openFixtureVaultTauri, removeFixtureVaultCopy } from '../helpers/fixtureVault'
import { triggerMenuCommand } from './testBridge'
const EDITOR_CRASH_FRAGMENTS = [
'#185',
'Maximum update depth',
'fillBefore',
'pasteHTML',
'RangeError',
]
interface TitleInput {
title: string
}
interface VaultPathInput {
vaultPath: string
}
interface FileContentExpectation extends VaultPathInput {
filename: string
text: string
}
interface RichHeadingPastePayload {
html: string
text: string
}
function slugifyTitle({ title }: TitleInput): string {
return title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '')
}
function markdownFiles({ vaultPath }: VaultPathInput): string[] {
return fs.readdirSync(vaultPath).filter((name) => name.endsWith('.md')).sort()
}
function collectEditorCrashMessages(page: Page): string[] {
const messages: string[] = []
const collect = (message: string) => {
if (EDITOR_CRASH_FRAGMENTS.some((fragment) => message.includes(fragment))) {
messages.push(message)
}
}
page.on('pageerror', (error) => collect(error.message))
page.on('console', (message) => {
if (message.type() === 'error') collect(message.text())
})
return messages
}
async function expectEditorFocused(page: Page): Promise<void> {
await expect.poll(async () => page.evaluate(() => {
const active = document.activeElement as HTMLElement | null
return Boolean(active?.isContentEditable || active?.closest('[contenteditable="true"]'))
}), { timeout: 5_000 }).toBe(true)
}
async function createUntitledNote(page: Page): Promise<void> {
await page.locator('body').click()
await triggerMenuCommand(page, 'file-new-note')
await expect(page.locator('.bn-editor')).toBeVisible({ timeout: 5_000 })
await expect(page.getByTestId('breadcrumb-filename-trigger')).toContainText(/untitled-note-\d+(?:-\d+)?/i, {
timeout: 5_000,
})
await expectEditorFocused(page)
}
async function expectActiveFilename(page: Page, { title }: TitleInput): Promise<void> {
await expect(page.getByTestId('breadcrumb-filename-trigger')).toContainText(slugifyTitle({ title }), {
timeout: 10_000,
})
}
async function expectFileContentContains(expectation: FileContentExpectation): Promise<void> {
await expect(async () => {
expect(markdownFiles(expectation)).toContain(expectation.filename)
expect(fs.readFileSync(`${expectation.vaultPath}/${expectation.filename}`, 'utf-8')).toContain(expectation.text)
}).toPass({ timeout: 10_000 })
}
async function dispatchRichHeadingPaste(page: Page, payload: RichHeadingPastePayload): Promise<void> {
await page.evaluate(({ htmlFragment, textFragment }) => {
const target = document.querySelector<HTMLElement>(
[
'.bn-editor [data-content-type="heading"][data-level="1"] .bn-inline-content',
'.bn-editor [data-content-type="heading"]:not([data-level]) .bn-inline-content',
'.bn-editor h1',
].join(', '),
)
if (!target) throw new Error('Title heading paste target was not found')
const clipboardData = new DataTransfer()
clipboardData.setData('text/html', htmlFragment)
clipboardData.setData('text/plain', textFragment)
const event = new Event('paste', { bubbles: true, cancelable: true })
Object.defineProperty(event, 'clipboardData', { value: clipboardData })
if (target.dispatchEvent(event)) {
throw new Error('Rich title-heading paste was not intercepted')
}
}, { htmlFragment: payload.html, textFragment: payload.text })
}
let tempVaultDir: string
test.beforeEach(async ({ page }, testInfo) => {
testInfo.setTimeout(90_000)
tempVaultDir = createFixtureVaultCopy()
await openFixtureVaultTauri(page, tempVaultDir)
})
test.afterEach(async () => {
removeFixtureVaultCopy(tempVaultDir)
})
test('new-note first typing and rich heading paste remain stable through saves', async ({ page }) => {
const crashMessages = collectEditorCrashMessages(page)
const title = 'Sentry Fresh Paste Guard'
const filename = `${slugifyTitle({ title })}.md`
await createUntitledNote(page)
await page.keyboard.type(title, { delay: 30 })
await page.keyboard.press('Enter')
await page.keyboard.type('Initial body text survives the create-note save.', { delay: 30 })
await expectActiveFilename(page, { title })
await expectFileContentContains({
vaultPath: tempVaultDir,
filename,
text: 'Initial body text survives the create-note save.',
})
const titleHeading = page.locator('.bn-editor [data-content-type="heading"]').first()
await titleHeading.click()
await page.keyboard.press('End')
await dispatchRichHeadingPaste(page, {
text: ' Rich Paste Payload',
html: '<h1>Rich <em>Paste</em> Payload</h1><table><tr><td>structured</td></tr></table>',
})
await expect(titleHeading).toContainText('Sentry Fresh Paste Guard Rich Paste Payload')
await page.keyboard.press('Enter')
await page.keyboard.type('Editing continues after the heading paste.', { delay: 30 })
await triggerMenuCommand(page, 'file-save')
await expectFileContentContains({
vaultPath: tempVaultDir,
filename,
text: '# Sentry Fresh Paste Guard Rich Paste Payload',
})
await expectFileContentContains({
vaultPath: tempVaultDir,
filename,
text: 'Editing continues after the heading paste.',
})
expect(crashMessages).toEqual([])
})