216 lines
6.9 KiB
TypeScript
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([])
|
|
})
|