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 { 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 { 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 { await expect(page.getByTestId('breadcrumb-filename-trigger')).toContainText(slugifyTitle({ title }), { timeout: 10_000, }) } async function expectFileContentContains(expectation: FileContentExpectation): Promise { 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 { await page.evaluate(({ htmlFragment, textFragment }) => { const target = document.querySelector( [ '.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: '

Rich Paste Payload

structured
', }) 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([]) })