import { test, expect } from '@playwright/test' import fs from 'fs' import path from 'path' import { createFixtureVaultCopy, openFixtureVaultDesktopHarness, removeFixtureVaultCopy } from '../helpers/fixtureVault' import { openCommandPalette, sendShortcut } from './helpers' let tempVaultDir: string function writeFixtureNote(vaultPath: string, filename: string, content: string): string { const notePath = path.join(vaultPath, filename) fs.writeFileSync(notePath, content) return notePath } function toast(page: import('@playwright/test').Page) { return page.locator('.fixed.bottom-8') } async function createTypeFromCommandPalette(page: import('@playwright/test').Page, typeName: string): Promise { await openCommandPalette(page) await page.locator('input[placeholder="Type a command..."]').fill('new type') await page.keyboard.press('Enter') const dialog = page.getByRole('dialog', { name: 'Create New Type' }) const title = dialog.getByText('Create New Type', { exact: true }) const typeInput = dialog.getByPlaceholder('e.g. Recipe, Book, Habit...') await expect(title).toBeVisible() await typeInput.fill(typeName) await dialog.getByRole('button', { name: 'Create' }).click() await expect(dialog).toHaveCount(0) } test.describe('Collision-safe create flows', () => { test.beforeEach(async ({ page }) => { tempVaultDir = createFixtureVaultCopy() await page.setViewportSize({ width: 1600, height: 900 }) }) test.afterEach(() => { removeFixtureVaultCopy(tempVaultDir) }) test('missing-type creation keeps the dialog open when a root filename already exists', async ({ page }) => { const collidingPath = writeFixtureNote( tempVaultDir, 'hotel.md', '---\ntype: Note\n---\n# Existing Hotel Note\n', ) writeFixtureNote( tempVaultDir, 'hotel-guide.md', '---\ntype: Hotel\nstatus: Active\n---\n# Hotel Guide\n', ) await openFixtureVaultDesktopHarness(page, tempVaultDir) await page.getByText('Hotel Guide', { exact: true }).click() await sendShortcut(page, 'i', ['Control', 'Shift']) const warning = page.getByTestId('missing-type-warning') await expect(warning).toBeVisible() await warning.focus() await page.keyboard.press('Enter') const dialog = page.getByRole('dialog') await expect(dialog).toBeVisible() await expect(dialog.getByPlaceholder('e.g. Recipe, Book, Habit...')).toHaveValue('Hotel') await page.keyboard.press('Enter') await expect(dialog).toBeVisible() await expect(toast(page)).toContainText('Cannot create type "Hotel" because hotel.md already exists') expect(fs.readFileSync(collidingPath, 'utf8')).toContain('# Existing Hotel Note') }) test('relationship create-and-open keeps the inline input active on filename collision', async ({ page }) => { const collidingPath = writeFixtureNote( tempVaultDir, 'briefing.md', '---\ntype: Note\n---\n# Weekly Sync\n', ) await openFixtureVaultDesktopHarness(page, tempVaultDir) await page.getByText('Team Meeting', { exact: true }).click() await sendShortcut(page, 'i', ['Control', 'Shift']) const addButton = page.getByTestId('add-relation-ref').first() await expect(addButton).toBeVisible() await addButton.click() const input = page.getByTestId('add-relation-ref-input') await expect(input).toBeVisible() await input.fill('Briefing!') await page.waitForTimeout(300) await page.getByTestId('create-and-open-option').click() await expect(input).toBeVisible() await expect(toast(page)).toContainText('Cannot create note "Briefing!" because briefing.md already exists') expect(fs.readFileSync(collidingPath, 'utf8')).toContain('# Weekly Sync') }) test('command palette type creation handles the built-in Note type without an unhandled write', async ({ page }) => { const defaultNoteTypePath = writeFixtureNote( tempVaultDir, 'note.md', '---\ntype: Type\n---\n# Note\n', ) await openFixtureVaultDesktopHarness(page, tempVaultDir) await openCommandPalette(page) await page.locator('input[placeholder="Type a command..."]').fill('new type') await page.keyboard.press('Enter') const dialog = page.getByRole('dialog', { name: 'Create New Type' }) const typeInput = dialog.getByPlaceholder('e.g. Recipe, Book, Habit...') await expect(dialog.getByText('Create New Type', { exact: true })).toBeVisible() await typeInput.fill('Note') await dialog.getByRole('button', { name: 'Create' }).click() await expect(dialog).toBeVisible() await expect(toast(page)).toContainText('Type "Note" already exists') expect(fs.readFileSync(defaultNoteTypePath, 'utf8')).toContain('# Note') }) test('command palette type creation maps the built-in Notes label away from notes.md collisions', async ({ page }) => { const existingNotesPath = writeFixtureNote( tempVaultDir, 'notes.md', '---\ntype: Note\n---\n# Meeting Notes\n', ) fs.rmSync(path.join(tempVaultDir, 'type', 'note.md'), { force: true }) const defaultNoteTypePath = path.join(tempVaultDir, 'note.md') await openFixtureVaultDesktopHarness(page, tempVaultDir) await createTypeFromCommandPalette(page, 'Notes') await expect.poll(() => fs.existsSync(defaultNoteTypePath)).toBe(true) await expect(toast(page)).toContainText('Type "Notes" created') expect(fs.readFileSync(defaultNoteTypePath, 'utf8')).toContain('# Note') expect(fs.readFileSync(existingNotesPath, 'utf8')).toContain('# Meeting Notes') }) test('unicode type creation ignores an unrelated untitled draft filename', async ({ page }) => { const untitledPath = writeFixtureNote( tempVaultDir, 'untitled.md', '---\ntype: Note\n---\n# Existing Untitled Note\n', ) const createdTypePath = path.join(tempVaultDir, '停智慧.md') await openFixtureVaultDesktopHarness(page, tempVaultDir) await createTypeFromCommandPalette(page, '停智慧') await expect.poll(() => fs.existsSync(createdTypePath)).toBe(true) expect(fs.readFileSync(createdTypePath, 'utf8')).toContain('type: Type') expect(fs.readFileSync(untitledPath, 'utf8')).toContain('# Existing Untitled Note') }) })