openpencil/tests/engine/vue/document-workspace.test.ts
Danila Poyarkov 453b27f15f perf(properties): retain inactive panels safely
Retain one selection-property subtree while frame presets are shown. Suspend opted-in descendant scopes after cleanup, cancel drafts before detachment can blur inputs, close portals, and release previews and pending resource work.

Track focused-element removal with VueUse so WebKit shortcuts resume after input teardown. Cover retained identity, hidden inactivity, binding rollback, async disposal, undo, popup reactivation and browser focus behavior.
2026-09-15 17:01:44 +03:00

203 lines
6.6 KiB
TypeScript

import { afterEach, describe, expect, test, vi } from 'bun:test'
import { defineComponent, h, type ComponentPublicInstance } from 'vue'
import {
useDocumentWorkspace,
type DocumentWorkspaceItem,
type DocumentWorkspaceSource
} from '@open-pencil/vue'
import { createTestRenderer, hostNode } from '#tests/helpers/vue/renderer'
type Deferred<Value> = {
promise: Promise<Value>
resolve(value: Value): void
}
function deferred<Value>(): Deferred<Value> {
let resolvePromise: ((value: Value) => void) | null = null
const promise = new Promise<Value>((resolve) => {
resolvePromise = resolve
})
return {
promise,
resolve(value) {
resolvePromise?.(value)
}
}
}
const renderer = createTestRenderer()
type Workspace = ReturnType<typeof useDocumentWorkspace<DocumentWorkspaceItem>>
type WorkspaceHolder = { current: Workspace | null }
async function flushTasks(): Promise<void> {
await new Promise<void>((resolve) => {
setTimeout(resolve, 0)
})
}
function mountWorkspace(
source: DocumentWorkspaceSource<DocumentWorkspaceItem>,
options: { previewConcurrency?: number } = {}
): { workspace: Workspace; unmount(): void } {
const holder: WorkspaceHolder = { current: null }
const component = defineComponent({
setup() {
holder.current = useDocumentWorkspace({
source,
previewConcurrency: options.previewConcurrency,
refreshOnFocus: false,
refreshOnReconnect: false
})
return () => h('div')
}
})
const app = renderer.createApp(component)
app.mount(hostNode()) as ComponentPublicInstance
const workspace = holder.current
if (workspace == null) throw new Error('Workspace composable did not initialize')
return { workspace, unmount: () => app.unmount() }
}
afterEach(() => {
vi.restoreAllMocks()
})
describe('useDocumentWorkspace', () => {
test('shares an in-flight refresh and responds to source events', async () => {
const firstRefresh = deferred<DocumentWorkspaceItem[]>()
let sourceListener: (() => void) | null = null
const refresh = vi
.fn<() => Promise<DocumentWorkspaceItem[]>>()
.mockImplementationOnce(() => firstRefresh.promise)
.mockResolvedValueOnce([{ id: 'second', name: 'Second', updatedAt: '2026-08-10' }])
const mounted = mountWorkspace({
refresh,
loadPreview: async () => null,
subscribe(listener) {
sourceListener = listener
return () => {
sourceListener = null
}
}
})
const inFlightA = mounted.workspace.refresh()
const inFlightB = mounted.workspace.refresh()
expect(inFlightA).toBe(inFlightB)
expect(refresh).toHaveBeenCalledTimes(1)
sourceListener?.()
sourceListener?.()
firstRefresh.resolve([{ id: 'first', name: 'First', updatedAt: '2026-08-09' }])
await inFlightA
await flushTasks()
expect(refresh).toHaveBeenCalledTimes(2)
expect(mounted.workspace.documents.value.map(({ id }) => id)).toEqual(['second'])
mounted.unmount()
expect(sourceListener).toBeNull()
})
test('invalidates an in-flight preview when the document changes', async () => {
const previewLoad = deferred<Uint8Array | null>()
const freshPreviewLoad = deferred<Uint8Array | null>()
const refresh = vi
.fn<() => Promise<DocumentWorkspaceItem[]>>()
.mockResolvedValueOnce([{ id: 'one', name: 'One', updatedAt: 'first' }])
.mockResolvedValueOnce([{ id: 'one', name: 'One', updatedAt: 'second' }])
const loadPreview = vi
.fn<() => Promise<Uint8Array | null>>()
.mockImplementationOnce(() => previewLoad.promise)
.mockImplementationOnce(() => freshPreviewLoad.promise)
const createObjectURL = vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:preview')
const mounted = mountWorkspace({ refresh, loadPreview })
await flushTasks()
mounted.workspace.loadPreview('one')
expect(loadPreview).toHaveBeenCalledTimes(1)
await mounted.workspace.refresh()
previewLoad.resolve(new Uint8Array([1]))
await previewLoad.promise
await flushTasks()
expect(createObjectURL).not.toHaveBeenCalled()
expect(loadPreview).toHaveBeenCalledTimes(2)
freshPreviewLoad.resolve(new Uint8Array([2]))
await freshPreviewLoad.promise
await flushTasks()
expect(createObjectURL).toHaveBeenCalledTimes(1)
mounted.unmount()
})
test('surfaces preview failures to consumers', async () => {
const failure = new Error('preview failed')
const onPreviewError = vi.fn()
const holder: WorkspaceHolder = { current: null }
const component = defineComponent({
setup() {
holder.current = useDocumentWorkspace({
source: {
refresh: async () => [],
loadPreview: async () => {
throw failure
}
},
refreshOnFocus: false,
refreshOnReconnect: false,
onPreviewError
})
return () => h('div')
}
})
const app = renderer.createApp(component)
app.mount(hostNode())
holder.current?.loadPreview('one')
await flushTasks()
expect(holder.current?.previewErrors.value.one).toBe(failure)
expect(onPreviewError).toHaveBeenCalledWith('one', failure)
app.unmount()
})
test('deduplicates previews, limits concurrency, and revokes URLs on unmount', async () => {
const loads = new Map<string, Deferred<Uint8Array | null>>()
const loadPreview = vi.fn((id: string) => {
const load = deferred<Uint8Array | null>()
loads.set(id, load)
return load.promise
})
const createObjectURL = vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:preview')
const revokeObjectURL = vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => undefined)
const mounted = mountWorkspace(
{ refresh: async () => [], loadPreview },
{ previewConcurrency: 2 }
)
mounted.workspace.loadPreview('one')
mounted.workspace.loadPreview('one')
mounted.workspace.loadPreview('two')
mounted.workspace.loadPreview('three')
expect(loadPreview.mock.calls.map((call) => call[0])).toEqual(['one', 'two'])
loads.get('one')?.resolve(new Uint8Array([1]))
await loads.get('one')?.promise
await flushTasks()
expect(loadPreview.mock.calls.map((call) => call[0])).toEqual(['one', 'two', 'three'])
expect(createObjectURL).toHaveBeenCalledTimes(1)
mounted.unmount()
expect(revokeObjectURL).toHaveBeenCalledWith('blob:preview')
loads.get('two')?.resolve(new Uint8Array([2]))
loads.get('three')?.resolve(new Uint8Array([3]))
await Promise.all([loads.get('two')?.promise, loads.get('three')?.promise])
await flushTasks()
expect(createObjectURL).toHaveBeenCalledTimes(1)
})
})