test: add Playwright + Vitest tests for raw editor type propagation
Verifies that editing the type field in the raw editor (Cmd+\) immediately updates the Properties panel type selector without navigation or reload. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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()
|
||||
|
||||
|
||||
93
tests/smoke/raw-editor-type-propagation.spec.ts
Normal file
93
tests/smoke/raw-editor-type-propagation.spec.ts
Normal file
@@ -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)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user