From 92f0f96ad24a0ff537115cb98c5ce92f1ceb2fa8 Mon Sep 17 00:00:00 2001 From: lucaronin Date: Wed, 11 Mar 2026 17:44:19 +0100 Subject: [PATCH] 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 --- .../smoke/fix-note-filename-on-rename.spec.ts | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 tests/smoke/fix-note-filename-on-rename.spec.ts diff --git a/tests/smoke/fix-note-filename-on-rename.spec.ts b/tests/smoke/fix-note-filename-on-rename.spec.ts new file mode 100644 index 00000000..0f88c831 --- /dev/null +++ b/tests/smoke/fix-note-filename-on-rename.spec.ts @@ -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([]) + }) +})