Files
tolaria/tests/smoke/create-note-backing-file.spec.ts

183 lines
6.5 KiB
TypeScript

import fs from 'fs'
import { test, expect, type Page } from '@playwright/test'
import { createFixtureVaultCopy, openFixtureVaultTauri, removeFixtureVaultCopy } from '../helpers/fixtureVault'
import { triggerMenuCommand } from './testBridge'
interface CreateNoteProbe {
createCalls: string[]
startedCreateCalls: string[]
getBeforeCreate: string[]
activeCreateCount: number
maxConcurrentCreates: number
}
interface ProbeWindow {
__mockHandlers?: Record<string, (args?: unknown) => unknown>
__createNoteBackingFileProbe?: CreateNoteProbe
}
let tempVaultDir: string
const CREATE_NOTE_WRITE_DELAY_MS = 350
async function pinFixtureHandlers(page: Page): Promise<void> {
await page.evaluate(() => {
const probeWindow = window as typeof window & ProbeWindow
const handlers = probeWindow.__mockHandlers
if (!handlers?.create_note_content || !handlers.get_note_content) {
throw new Error('Fixture vault handlers are missing create/read commands')
}
Object.defineProperty(window, '__mockHandlers', {
configurable: true,
get: () => handlers,
set: (nextHandlers) => Object.assign(handlers, nextHandlers),
})
})
}
async function recordCreateNoteCalls(page: Page): Promise<void> {
await page.evaluate((writeDelayMs) => {
const probeWindow = window as typeof window & ProbeWindow
const handlers = probeWindow.__mockHandlers as Record<string, (args?: unknown) => unknown>
const originalCreate = handlers.create_note_content.bind(handlers)
const probe: CreateNoteProbe = {
activeCreateCount: 0,
createCalls: [],
getBeforeCreate: [],
maxConcurrentCreates: 0,
startedCreateCalls: [],
}
probeWindow.__createNoteBackingFileProbe = probe
handlers.create_note_content = async (args?: unknown) => {
const notePath = String((args as { path?: unknown } | undefined)?.path ?? '')
probe.startedCreateCalls.push(notePath)
probe.activeCreateCount += 1
probe.maxConcurrentCreates = Math.max(probe.maxConcurrentCreates, probe.activeCreateCount)
try {
await new Promise((resolve) => setTimeout(resolve, writeDelayMs))
return await originalCreate(args)
} finally {
probe.activeCreateCount -= 1
probe.createCalls.push(notePath)
}
}
}, CREATE_NOTE_WRITE_DELAY_MS)
}
async function rejectReadsBeforeCreate(page: Page): Promise<void> {
await page.evaluate(() => {
const probeWindow = window as typeof window & ProbeWindow
const handlers = probeWindow.__mockHandlers as Record<string, (args?: unknown) => unknown>
const originalGet = handlers.get_note_content.bind(handlers)
const probe = probeWindow.__createNoteBackingFileProbe as CreateNoteProbe
handlers.get_note_content = (args?: unknown) => {
const notePath = String((args as { path?: unknown } | undefined)?.path ?? '')
if (notePath.includes('untitled-note-') && !probe.createCalls.includes(notePath)) {
probe.getBeforeCreate.push(notePath)
throw new Error(`File does not exist: ${notePath}`)
}
return originalGet(args)
}
})
}
async function installCreateNoteBackingFileProbe(page: Page): Promise<void> {
await pinFixtureHandlers(page)
await recordCreateNoteCalls(page)
await rejectReadsBeforeCreate(page)
}
async function readProbe(page: Page): Promise<CreateNoteProbe> {
return page.evaluate(() => {
const probeWindow = window as typeof window & ProbeWindow
return probeWindow.__createNoteBackingFileProbe ?? {
activeCreateCount: 0,
createCalls: [],
getBeforeCreate: [],
maxConcurrentCreates: 0,
startedCreateCalls: [],
}
})
}
async function dispatchMenuCommandBurst(page: Page, commandId: string, count: number): Promise<void> {
await page.waitForFunction(
() => typeof window.__laputaTest?.dispatchBrowserMenuCommand === 'function',
undefined,
{ timeout: 5_000 },
)
await page.evaluate(({ commandId: id, count: commandCount }) => {
const testWindow = window as typeof window & {
__laputaTest?: { dispatchBrowserMenuCommand?: (commandId: string) => void }
}
const dispatchBrowserMenuCommand = testWindow.__laputaTest?.dispatchBrowserMenuCommand
if (typeof dispatchBrowserMenuCommand !== 'function') {
throw new Error('Tolaria test bridge is missing dispatchBrowserMenuCommand')
}
for (let index = 0; index < commandCount; index += 1) {
dispatchBrowserMenuCommand(id)
}
}, { commandId, count })
}
test.beforeEach(async ({ page }, testInfo) => {
testInfo.setTimeout(60_000)
tempVaultDir = createFixtureVaultCopy()
await openFixtureVaultTauri(page, tempVaultDir)
await installCreateNoteBackingFileProbe(page)
})
test.afterEach(() => {
removeFixtureVaultCopy(tempVaultDir)
})
test('@smoke creating a note writes its backing file before reload can read it', async ({ page }) => {
const errors: string[] = []
page.on('pageerror', (error) => errors.push(error.message))
await triggerMenuCommand(page, 'file-new-note')
await triggerMenuCommand(page, 'vault-reload')
await expect(page.getByTestId('breadcrumb-filename-trigger')).toContainText(/untitled-note-\d+/i, {
timeout: 5_000,
})
await expect.poll(() => readProbe(page), { timeout: 5_000 }).toMatchObject({
createCalls: [expect.stringMatching(/untitled-note-\d+\.md$/)],
getBeforeCreate: [],
})
const { createCalls, getBeforeCreate } = await readProbe(page)
expect(getBeforeCreate).toEqual([])
expect(errors.filter((message) => message.includes('File does not exist'))).toEqual([])
expect(fs.readFileSync(createCalls[0], 'utf8')).toContain('type: Note')
})
test('@smoke rapid note creation waits for each backing file write', async ({ page }) => {
const errors: string[] = []
page.on('pageerror', (error) => errors.push(error.message))
await dispatchMenuCommandBurst(page, 'file-new-note', 2)
await expect.poll(() => readProbe(page), { timeout: 7_000 }).toMatchObject({
createCalls: [
expect.stringMatching(/untitled-note-\d+\.md$/),
expect.stringMatching(/untitled-note-\d+(?:-\d+)?\.md$/),
],
getBeforeCreate: [],
maxConcurrentCreates: 1,
startedCreateCalls: [
expect.stringMatching(/untitled-note-\d+\.md$/),
expect.stringMatching(/untitled-note-\d+(?:-\d+)?\.md$/),
],
})
const { createCalls, getBeforeCreate, maxConcurrentCreates } = await readProbe(page)
expect(new Set(createCalls).size).toBe(2)
expect(getBeforeCreate).toEqual([])
expect(maxConcurrentCreates).toBe(1)
expect(errors.filter((message) => message.includes('#185') || message.includes('Maximum update depth'))).toEqual([])
})