Files
tolaria/tests/integration/vault-workflows.spec.ts
Test 13622bc236 test: add real filesystem vault integration tests
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>
2026-03-11 21:24:44 +01:00

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 })
})