diff --git a/src/hooks/useEditorSaveWithLinks.test.ts b/src/hooks/useEditorSaveWithLinks.test.ts index e2734450..fd86266e 100644 --- a/src/hooks/useEditorSaveWithLinks.test.ts +++ b/src/hooks/useEditorSaveWithLinks.test.ts @@ -125,6 +125,42 @@ describe('useEditorSaveWithLinks', () => { }) }) + it('handleContentChange calls updateEntry with frontmatter patch when type changes', () => { + const { result } = renderHookWithLinks() + + act(() => { + result.current.handleContentChange('/note.md', '---\ntype: Project\nstatus: Active\n---\nBody') + }) + + expect(updateEntry).toHaveBeenCalledWith('/note.md', { isA: 'Project', status: 'Active' }) + }) + + it('handleContentChange does NOT call updateEntry for frontmatter when unchanged', () => { + const { result } = renderHookWithLinks() + const content = '---\ntype: Essay\n---\nBody text' + + act(() => { result.current.handleContentChange('/note.md', content) }) + const callCount = updateEntry.mock.calls.length + + act(() => { result.current.handleContentChange('/note.md', content + ' more') }) + // Same frontmatter, only body changed — no extra updateEntry for frontmatter + expect(updateEntry).toHaveBeenCalledTimes(callCount) + }) + + it('handleContentChange updates entry when type changes in frontmatter', () => { + const { result } = renderHookWithLinks() + + act(() => { + result.current.handleContentChange('/note.md', '---\ntype: Essay\n---\nBody') + }) + expect(updateEntry).toHaveBeenCalledWith('/note.md', { isA: 'Essay' }) + + act(() => { + result.current.handleContentChange('/note.md', '---\ntype: Note\n---\nBody') + }) + expect(updateEntry).toHaveBeenCalledWith('/note.md', { isA: 'Note' }) + }) + it('spreads all properties from useEditorSave onto the return value', () => { const { result } = renderHookWithLinks() diff --git a/tests/smoke/raw-editor-type-propagation.spec.ts b/tests/smoke/raw-editor-type-propagation.spec.ts new file mode 100644 index 00000000..1417cee2 --- /dev/null +++ b/tests/smoke/raw-editor-type-propagation.spec.ts @@ -0,0 +1,93 @@ +import { test, expect } from '@playwright/test' + +test.describe('Raw editor type propagation', () => { + test.beforeEach(async ({ page }) => { + await page.setViewportSize({ width: 1600, height: 900 }) + await page.goto('/') + await page.waitForLoadState('networkidle') + }) + + test('editing type in raw editor immediately updates Properties panel', async ({ page }) => { + const noteList = page.locator('[data-testid="note-list-container"]') + await noteList.waitFor({ timeout: 5000 }) + const items = noteList.locator('.cursor-pointer') + const count = await items.count() + test.skip(count === 0, 'No notes in note list') + + // Click first note and open Properties panel + await items.nth(0).click() + await page.waitForTimeout(300) + await page.keyboard.press('Control+Shift+i') + await page.waitForTimeout(500) + + // Find a note with a visible type selector (skip Theme) + const typeSelector = page.locator('[data-testid="type-selector"]') + let originalType = '' + for (let i = 0; i < Math.min(count, 10); i++) { + await items.nth(i).click() + await page.waitForTimeout(400) + if (!(await typeSelector.isVisible())) continue + const trigger = typeSelector.locator('button[role="combobox"]') + const text = (await trigger.textContent())?.trim() ?? '' + if (text && !text.includes('Theme')) { + originalType = text + break + } + } + test.skip(!originalType, 'No non-Theme note with type selector found') + + // Open raw editor (Ctrl+\) + await page.keyboard.press('Control+Backslash') + const rawEditor = page.locator('[data-testid="raw-editor-codemirror"]') + await expect(rawEditor).toBeVisible({ timeout: 3000 }) + await page.waitForTimeout(300) + + // Find the "type:" line in the editor + const typeLineIndex = await page.evaluate(() => { + const lines = document.querySelectorAll('.cm-line') + for (let i = 0; i < lines.length; i++) { + if (lines[i].textContent?.match(/^(?:type|Is A):\s/i)) return i + } + return -1 + }) + test.skip(typeLineIndex < 0, 'No type field found in frontmatter') + + // Click the type line, select it, and retype with new type + const typeLine = page.locator('.cm-line').nth(typeLineIndex) + await typeLine.click() + await page.waitForTimeout(100) + await page.keyboard.press('Home') + await page.keyboard.press('Shift+End') + + const newType = originalType.includes('Note') ? 'Project' : 'Note' + await page.keyboard.type(`type: ${newType}`) + + // Wait for debounce (500ms) + state propagation + await page.waitForTimeout(800) + + // Verify Properties panel shows the new type + const trigger = typeSelector.locator('button[role="combobox"]') + await expect(trigger).toContainText(newType, { timeout: 3000 }) + + // Restore: select the type line and retype original + const restoreLineIndex = await page.evaluate(() => { + const lines = document.querySelectorAll('.cm-line') + for (let i = 0; i < lines.length; i++) { + if (lines[i].textContent?.match(/^type:\s/)) return i + } + return -1 + }) + if (restoreLineIndex >= 0) { + const restoreLine = page.locator('.cm-line').nth(restoreLineIndex) + await restoreLine.click() + await page.keyboard.press('Home') + await page.keyboard.press('Shift+End') + await page.keyboard.type(`type: ${originalType.replace(/Note$/, '')}`) + await page.waitForTimeout(800) + } + + // Close raw editor + await page.keyboard.press('Control+Backslash') + await page.waitForTimeout(300) + }) +})