Files
tolaria/tests/smoke/vault-switch-clears-stale-editor.spec.ts
2026-05-07 00:25:15 +02:00

216 lines
6.9 KiB
TypeScript

import { test, expect, type Page } from '@playwright/test'
import { executeCommand, openCommandPalette } from './helpers'
interface MockEntry {
path: string
filename: string
title: string
isA: string
aliases: string[]
belongsTo: string[]
relatedTo: string[]
status: string | null
archived: boolean
modifiedAt: number
createdAt: number | null
fileSize: number
snippet: string
wordCount: number
relationships: Record<string, string[]>
outgoingLinks: string[]
properties: Record<string, unknown>
template: null
sort: null
}
interface SaveCall {
path: string
content: string
activeVaultPath: string
}
interface SmokeVaultData {
contentByPath: Record<string, string>
entriesByVault: Record<string, MockEntry[]>
personalVaultPath: string
workVaultPath: string
}
interface CodeMirrorView {
state: { doc: { length: number } }
dispatch: (transaction: { changes: { from: number; to: number; insert: string } }) => void
}
interface CodeMirrorElement extends Element {
cmTile?: { view?: CodeMirrorView }
}
type MockHandler = (args?: Record<string, unknown>) => unknown
function makeEntry(vaultPath: string, filename: string, title: string): MockEntry {
return {
path: `${vaultPath}/${filename}`,
filename,
title,
isA: 'Note',
aliases: [],
belongsTo: [],
relatedTo: [],
status: null,
archived: false,
modifiedAt: 1_700_000_000,
createdAt: null,
fileSize: 128,
snippet: `${title} body`,
wordCount: 4,
relationships: {},
outgoingLinks: [],
properties: {},
template: null,
sort: null,
}
}
function buildSmokeVaultData(): SmokeVaultData {
const workVaultPath = '/Users/mock/Work'
const personalVaultPath = '/Users/mock/Personal'
const workEntry = makeEntry(workVaultPath, 'work-home.md', 'Work Home')
const personalEntry = makeEntry(personalVaultPath, 'personal-home.md', 'Personal Home')
return {
workVaultPath,
personalVaultPath,
entriesByVault: {
[workVaultPath]: [workEntry],
[personalVaultPath]: [personalEntry],
},
contentByPath: {
[workEntry.path]: '# Work Home\n\nWork body',
[personalEntry.path]: '# Personal Home\n\nPersonal body',
},
}
}
async function installVaultSwitchMocks(page: Page): Promise<SmokeVaultData> {
const data = buildSmokeVaultData()
await page.addInitScript((data: SmokeVaultData) => {
localStorage.clear()
localStorage.setItem('tolaria_welcome_dismissed', '1')
localStorage.setItem('tolaria:claude-code-onboarding-dismissed', '1')
let activeVaultPath = data.workVaultPath
let handlers: Record<string, MockHandler> | null = null
const saveCalls: SaveCall[] = []
const contentByPath = { ...data.contentByPath }
const vaultPaths = new Set([data.workVaultPath, data.personalVaultPath])
const smokeWindow = window as Window & { __staleVaultSaveProbe?: SaveCall[] }
smokeWindow.__staleVaultSaveProbe = saveCalls
Object.defineProperty(window, '__mockHandlers', {
configurable: true,
set(value: unknown) {
handlers = value as Record<string, MockHandler>
handlers.load_vault_list = () => ({
vaults: [
{ label: 'Work Vault', path: data.workVaultPath },
{ label: 'Personal Vault', path: data.personalVaultPath },
],
active_vault: activeVaultPath,
hidden_defaults: [],
})
handlers.save_vault_list = (args = {}) => {
const list = args.list as { active_vault?: unknown }
if (typeof list.active_vault === 'string') activeVaultPath = list.active_vault
return null
}
handlers.check_vault_exists = (args = {}) => vaultPaths.has(String(args.path))
handlers.get_default_vault_path = () => data.workVaultPath
handlers.list_vault = (args = {}) => data.entriesByVault[String(args.path)] ?? []
handlers.reload_vault = handlers.list_vault
handlers.list_vault_folders = () => []
handlers.list_views = () => []
handlers.get_note_content = (args = {}) => contentByPath[String(args.path)] ?? ''
handlers.validate_note_content = () => true
handlers.save_note_content = (args = {}) => {
const path = String(args.path)
const content = String(args.content)
saveCalls.push({ path, content, activeVaultPath })
contentByPath[path] = content
return null
}
handlers.get_modified_files = () => []
handlers.get_file_history = () => []
handlers.is_git_repo = () => true
handlers.sync_mcp_bridge_vault = () => null
},
get() {
return handlers
},
})
}, data)
return data
}
async function openNote(page: Page, title: string): Promise<void> {
await page.getByTestId('note-list-container').getByText(title, { exact: true }).click()
}
async function openRawMode(page: Page): Promise<void> {
await openCommandPalette(page)
await executeCommand(page, 'Toggle Raw')
await expect(page.locator('.cm-content')).toBeVisible({ timeout: 5_000 })
}
async function setRawEditorContent(page: Page, content: string): Promise<void> {
await page.evaluate((nextContent) => {
const element = document.querySelector('.cm-content') as CodeMirrorElement | null
const view = element?.cmTile?.view
if (!view) throw new Error('CodeMirror view is missing')
view.dispatch({
changes: { from: 0, to: view.state.doc.length, insert: nextContent },
})
}, content)
}
async function readSaveProbe(page: Page): Promise<SaveCall[]> {
return page.evaluate(() => {
const smokeWindow = window as Window & { __staleVaultSaveProbe?: SaveCall[] }
return smokeWindow.__staleVaultSaveProbe ?? []
})
}
async function clearSaveProbe(page: Page): Promise<void> {
await page.evaluate(() => {
const smokeWindow = window as Window & { __staleVaultSaveProbe?: SaveCall[] }
if (smokeWindow.__staleVaultSaveProbe) smokeWindow.__staleVaultSaveProbe.length = 0
})
}
async function switchToPersonalVault(page: Page): Promise<void> {
await page.getByTestId('status-vault-trigger').click()
await page.getByTestId('vault-menu-item-Personal Vault').click()
}
test('switching vaults clears stale pending editor saves @smoke', async ({ page }) => {
const data = await installVaultSwitchMocks(page)
await page.goto('/', { waitUntil: 'domcontentloaded' })
await openNote(page, 'Work Home')
await openRawMode(page)
await setRawEditorContent(page, '# Work Home\n\nUnsaved draft before vault switch')
await switchToPersonalVault(page)
await expect(page.getByTestId('note-list-container').getByText('Personal Home', { exact: true })).toBeVisible()
expect(await readSaveProbe(page)).not.toEqual(expect.arrayContaining([
expect.objectContaining({
activeVaultPath: data.personalVaultPath,
path: `${data.workVaultPath}/work-home.md`,
}),
]))
await clearSaveProbe(page)
await page.waitForTimeout(1_800)
expect(await readSaveProbe(page)).toEqual([])
})