test: Playwright smoke test for note filename rename on save

Covers: title change + save renames file, no rename when filename matches,
rapid title edits rename to final title.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
lucaronin
2026-03-11 17:44:19 +01:00
parent e0bf514f5d
commit 92f0f96ad2

View File

@@ -0,0 +1,100 @@
import { test, expect } from '@playwright/test'
import { sendShortcut } from './helpers'
/** Known BlockNote/ProseMirror error that fires during editor re-mount after rename. */
const KNOWN_EDITOR_ERRORS = ['isConnected']
function isKnownEditorError(msg: string): boolean {
return KNOWN_EDITOR_ERRORS.some(k => msg.includes(k))
}
/**
* Helper: create a new note, select the existing H1 heading text,
* replace it with a new title, then wait for the 500 ms title-sync debounce.
*/
async function createNoteWithTitle(page: import('@playwright/test').Page, title: string) {
// 1. Cmd+N → new "Untitled note" (creates heading with "Untitled note")
await page.locator('body').click()
await sendShortcut(page, 'n', ['Control'])
await expect(page.getByText(/Untitled note/).first()).toBeVisible({ timeout: 3000 })
// 2. Wait for the heading to render in BlockNote
const heading = page.locator('[data-content-type="heading"] h1')
await heading.waitFor({ timeout: 3000 })
// 3. Triple-click the heading to select all its text, then type replacement
await heading.click({ clickCount: 3 })
await page.keyboard.type(title, { delay: 20 })
// 4. Wait for the 500 ms useHeadingTitleSync debounce to fire + React re-render
await page.waitForTimeout(800)
}
test.describe('Note filename updates on title change + save', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/')
await page.waitForLoadState('networkidle')
// Wait for startup toast ("Laputa registered as MCP tool") to dismiss
await page.waitForTimeout(2500)
})
test('Cmd+N creates untitled note, typing new title + Cmd+S renames file', async ({ page }) => {
const errors: string[] = []
page.on('pageerror', (err) => { if (!isKnownEditorError(err.message)) errors.push(err.message) })
await createNoteWithTitle(page, 'Test Note ABC')
// Breadcrumb should already show the new title (title sync fired)
const breadcrumb = page.locator('span.truncate.font-medium')
await expect(breadcrumb.first()).toContainText('Test Note ABC', { timeout: 2000 })
// 5. Cmd+S → save + rename
await sendShortcut(page, 's', ['Control'])
// 6. Toast should show "Renamed" (appears within 5s, auto-dismisses after 2s)
const toast = page.locator('.fixed.bottom-8')
await expect(toast).toContainText('Renamed', { timeout: 5000 })
// 7. No unexpected JS errors
expect(errors).toEqual([])
})
test('saving a note whose filename already matches does not trigger rename', async ({ page }) => {
// Click on an existing note in the note list that already has a matching filename.
// The default sidebar shows "Notes" section with several notes visible.
const noteItem = page.locator('.truncate.text-foreground.font-medium').filter({ hasText: /Refactoring/ }).first()
await noteItem.click()
await page.waitForTimeout(500)
// Cmd+S — should show "Saved" or "Nothing to save", NOT "Renamed"
await sendShortcut(page, 's', ['Control'])
const toast = page.locator('.fixed.bottom-8')
await expect(toast).toBeVisible({ timeout: 3000 })
const toastText = await toast.textContent()
expect(toastText).not.toContain('Renamed')
})
test('rapid title changes only rename to final title on Cmd+S', async ({ page }) => {
const errors: string[] = []
page.on('pageerror', (err) => { if (!isKnownEditorError(err.message)) errors.push(err.message) })
await createNoteWithTitle(page, 'First Title')
// Select heading text again and replace with final title
const heading = page.locator('[data-content-type="heading"] h1')
await heading.click({ clickCount: 3 })
await page.keyboard.type('Final Title', { delay: 20 })
// Wait for debounce again
await page.waitForTimeout(800)
// Cmd+S → should rename to final-title.md
await sendShortcut(page, 's', ['Control'])
const toast = page.locator('.fixed.bottom-8')
await expect(toast).toContainText('Renamed', { timeout: 5000 })
expect(errors).toEqual([])
})
})