Replace mock vault approach with real filesystem I/O for integration tests. Each test copies tests/fixtures/test-vault/ to a temp directory, overrides mock handlers to point at it, and verifies actual file operations through the vite dev server middleware. Tests cover: vault loading, archive/trash filtering, note creation, rename with filesystem update, wikilink cascade on rename, relationship display, and real file content loading. Also extends vite.config.ts vault API middleware with write endpoints (save, rename, delete, search, entry) and fixes getBool to handle YAML 1.2 string values like "Yes"/"yes" (js-yaml 4.x compatibility). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
231 lines
9.3 KiB
TypeScript
231 lines
9.3 KiB
TypeScript
/**
|
|
* Integration tests against a real filesystem vault.
|
|
*
|
|
* Each test copies tests/fixtures/test-vault/ to a temp directory,
|
|
* points the app at it, and verifies real file I/O: creation, rename,
|
|
* wikilink updates, archive/trash filtering, and relationship display.
|
|
*/
|
|
import { test, expect } from '@playwright/test'
|
|
import fs from 'fs'
|
|
import path from 'path'
|
|
import os from 'os'
|
|
|
|
const FIXTURE_VAULT = path.resolve('tests/fixtures/test-vault')
|
|
|
|
function copyDirSync(src: string, dest: string): void {
|
|
fs.mkdirSync(dest, { recursive: true })
|
|
for (const item of fs.readdirSync(src, { withFileTypes: true })) {
|
|
const s = path.join(src, item.name)
|
|
const d = path.join(dest, item.name)
|
|
if (item.isDirectory()) copyDirSync(s, d)
|
|
else fs.copyFileSync(s, d)
|
|
}
|
|
}
|
|
|
|
let tempVaultDir: string
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
// Fresh vault copy for each test
|
|
tempVaultDir = fs.mkdtempSync(path.join(os.tmpdir(), 'laputa-test-vault-'))
|
|
copyDirSync(FIXTURE_VAULT, tempVaultDir)
|
|
|
|
// Intercept window.__mockHandlers assignment to override vault path handlers.
|
|
// The Object.defineProperty setter fires when mock-tauri/index.ts sets
|
|
// window.__mockHandlers = mockHandlers, letting us patch the same object
|
|
// reference that mockInvoke reads from.
|
|
await page.addInitScript((vaultPath: string) => {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
let ref: any = null
|
|
Object.defineProperty(window, '__mockHandlers', {
|
|
set(val) {
|
|
ref = val
|
|
ref.load_vault_list = () => ({
|
|
vaults: [{ label: 'Test Vault', path: vaultPath }],
|
|
active_vault: vaultPath,
|
|
})
|
|
ref.check_vault_exists = () => true
|
|
ref.get_last_vault_path = () => vaultPath
|
|
ref.get_default_vault_path = () => vaultPath
|
|
ref.save_vault_list = () => null
|
|
},
|
|
get() { return ref },
|
|
configurable: true,
|
|
})
|
|
}, tempVaultDir)
|
|
|
|
await page.goto('/')
|
|
// Wait until the real vault entries are loaded in the sidebar
|
|
await page.getByText('Alpha Project', { exact: true }).first().waitFor({ timeout: 10_000 })
|
|
})
|
|
|
|
test.afterEach(async () => {
|
|
fs.rmSync(tempVaultDir, { recursive: true, force: true })
|
|
})
|
|
|
|
/** Helper: locate the note-list container. */
|
|
function noteList(page: import('@playwright/test').Page) {
|
|
return page.locator('[data-testid="note-list-container"]')
|
|
}
|
|
|
|
/** Helper: click a note item by title text in the sidebar. */
|
|
async function openNote(page: import('@playwright/test').Page, title: string) {
|
|
await noteList(page).getByText(title, { exact: true }).click()
|
|
await page.waitForTimeout(300)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 1. Vault loads entries from real fixture files
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test('vault loads entries from fixture files', async ({ page }) => {
|
|
const list = noteList(page)
|
|
await expect(list.getByText('Alpha Project', { exact: true })).toBeVisible()
|
|
await expect(list.getByText('Note B', { exact: true })).toBeVisible()
|
|
await expect(list.getByText('Note C', { exact: true })).toBeVisible()
|
|
await expect(list.getByText('Team Meeting', { exact: true })).toBeVisible()
|
|
|
|
// Open a note and verify editor shows its content from disk
|
|
await openNote(page, 'Alpha Project')
|
|
// The WYSIWYG editor renders the title as an H1 heading
|
|
await expect(page.getByRole('heading', { name: 'Alpha Project', level: 1 })).toBeVisible({ timeout: 5_000 })
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 2. Archived note hidden from main sidebar
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test('archived note does not appear in All Notes', async ({ page }) => {
|
|
const list = noteList(page)
|
|
// "Archived Note" has Archived: Yes — should be hidden from default view
|
|
await expect(list.getByText('Archived Note', { exact: true })).not.toBeVisible()
|
|
|
|
// But regular notes are visible
|
|
await expect(list.getByText('Note B', { exact: true })).toBeVisible()
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 3. Trashed note hidden from note list
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test('trashed note does not appear in All Notes', async ({ page }) => {
|
|
const list = noteList(page)
|
|
// "Trashed Note" has Trashed: true — should be hidden
|
|
await expect(list.getByText('Trashed Note', { exact: true })).not.toBeVisible()
|
|
|
|
// Regular notes are visible
|
|
await expect(list.getByText('Note C', { exact: true })).toBeVisible()
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 4. Create note saves file to disk with correct slug
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test('create note saves file to disk with correct slug', async ({ page }) => {
|
|
// "Create new note" instantly creates "Untitled note" and opens in editor
|
|
await page.locator('button[title="Create new note"]').click()
|
|
await page.waitForTimeout(500)
|
|
|
|
// Verify the new note opens — H1 heading should appear in the WYSIWYG editor
|
|
await expect(page.getByRole('heading', { name: /Untitled note/i, level: 1 })).toBeVisible({ timeout: 5_000 })
|
|
|
|
// Save to disk via Cmd+S
|
|
await page.keyboard.press('Meta+s')
|
|
|
|
// Poll for the file to appear on disk
|
|
const expectedPath = path.join(tempVaultDir, 'note', 'untitled-note.md')
|
|
await expect(async () => {
|
|
expect(fs.existsSync(expectedPath)).toBe(true)
|
|
}).toPass({ timeout: 5_000 })
|
|
|
|
const content = fs.readFileSync(expectedPath, 'utf-8')
|
|
expect(content).toContain('# Untitled note')
|
|
expect(content).toContain('type: Note')
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 5. Rename note updates filename on disk
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test('rename note updates filename on disk', async ({ page }) => {
|
|
// Open Note B
|
|
await openNote(page, 'Note B')
|
|
|
|
// Double-click the tab title — scope to the draggable tab element to avoid
|
|
// matching the breadcrumb span that also has class "truncate".
|
|
await page.locator('[draggable="true"] span.truncate', { hasText: 'Note B' }).dblclick()
|
|
await page.waitForTimeout(200)
|
|
|
|
// In rename mode, draggable becomes false and an <input> appears in the tab.
|
|
// It's the only <input> element on the page at this point.
|
|
const input = page.locator('input').first()
|
|
await expect(input).toBeVisible({ timeout: 2_000 })
|
|
await input.fill('Note B Renamed')
|
|
await input.press('Enter')
|
|
await page.waitForTimeout(500)
|
|
|
|
// Verify filesystem: old file gone, new file exists
|
|
const oldPath = path.join(tempVaultDir, 'note', 'note-b.md')
|
|
const newPath = path.join(tempVaultDir, 'note', 'note-b-renamed.md')
|
|
|
|
await expect(async () => {
|
|
expect(fs.existsSync(oldPath)).toBe(false)
|
|
expect(fs.existsSync(newPath)).toBe(true)
|
|
}).toPass({ timeout: 5_000 })
|
|
|
|
const newContent = fs.readFileSync(newPath, 'utf-8')
|
|
expect(newContent).toContain('# Note B Renamed')
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 6. Wikilink update on rename — other files' [[Note B]] updated
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test('rename note updates wikilinks in other files', async ({ page }) => {
|
|
// Open Note B and rename it
|
|
await openNote(page, 'Note B')
|
|
|
|
await page.locator('[draggable="true"] span.truncate', { hasText: 'Note B' }).dblclick()
|
|
await page.waitForTimeout(200)
|
|
|
|
const input = page.locator('input').first()
|
|
await expect(input).toBeVisible({ timeout: 2_000 })
|
|
await input.fill('Note B Updated')
|
|
await input.press('Enter')
|
|
|
|
// Wait for rename to complete (file to be moved)
|
|
const newPath = path.join(tempVaultDir, 'note', 'note-b-updated.md')
|
|
await expect(async () => {
|
|
expect(fs.existsSync(newPath)).toBe(true)
|
|
}).toPass({ timeout: 5_000 })
|
|
|
|
// Verify alpha-project.md now references [[Note B Updated]] instead of [[Note B]]
|
|
const alphaContent = fs.readFileSync(path.join(tempVaultDir, 'project', 'alpha-project.md'), 'utf-8')
|
|
expect(alphaContent).toContain('[[Note B Updated]]')
|
|
expect(alphaContent).not.toContain('[[Note B]]')
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 7. Relationship display in inspector
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test('inspector shows relationships for note with wikilink fields', async ({ page }) => {
|
|
// Open Note C which has Related to: [[Alpha Project]] and Topics: [[design]]
|
|
await openNote(page, 'Note C')
|
|
|
|
// The DynamicRelationshipsPanel renders field labels like "Related to", "Topics"
|
|
// as <span> elements. Verify that the relationship labels are visible in the inspector.
|
|
await expect(page.getByText('Related to').first()).toBeVisible({ timeout: 5_000 })
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 8. Opening a note loads real file content from disk
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test('editor shows real file content from disk', async ({ page }) => {
|
|
// Open Team Meeting
|
|
await openNote(page, 'Team Meeting')
|
|
|
|
// Verify editor shows the actual file content — H1 heading from the file
|
|
await expect(page.getByRole('heading', { name: 'Team Meeting', level: 1 })).toBeVisible({ timeout: 5_000 })
|
|
})
|