e0a5f57793
Vendor the rebranded BoosAPI Desktop client (Tauri + Vue) into metapi as a monorepo subdirectory under desktop/. Source only — node_modules, dist, Cargo target and the upstream .git history are excluded. desktop/ keeps its own independent Cargo workspace (desktop/src-tauri with crates/*); it is not part of the backend boosapi crate, so a `cargo check` at the metapi root still compiles only the backend. Build the desktop app from desktop/src-tauri per desktop/README.md. Note: app icons are still placeholder assets pending the final BoosAPI logo. The updater signing keys live outside the repo and are not included.
149 lines
4.8 KiB
TypeScript
149 lines
4.8 KiB
TypeScript
import { mount } from '@vue/test-utils'
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { flushAll } from './setup/flush-all'
|
|
|
|
interface Entry { target: Element, isIntersecting: boolean, intersectionRatio: number }
|
|
interface ObserverInit {
|
|
root?: Element | Document | null
|
|
rootMargin?: string
|
|
threshold?: number | number[]
|
|
}
|
|
|
|
class FakeIntersectionObserver {
|
|
static instances: FakeIntersectionObserver[] = []
|
|
callback: (entries: Entry[]) => void
|
|
elements = new Set<Element>()
|
|
options: ObserverInit
|
|
|
|
constructor(cb: (entries: Entry[]) => void, options: ObserverInit = {}) {
|
|
this.callback = cb
|
|
this.options = options
|
|
FakeIntersectionObserver.instances.push(this)
|
|
}
|
|
|
|
observe(el: Element) {
|
|
this.elements.add(el)
|
|
}
|
|
|
|
unobserve(el: Element) {
|
|
this.elements.delete(el)
|
|
}
|
|
|
|
disconnect() {
|
|
this.elements.clear()
|
|
}
|
|
|
|
trigger(el: Element, isIntersecting = true) {
|
|
if (!this.elements.has(el))
|
|
return
|
|
this.callback([{ target: el, isIntersecting, intersectionRatio: isIntersecting ? 1 : 0 }])
|
|
}
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.useRealTimers()
|
|
vi.unstubAllGlobals()
|
|
vi.resetModules()
|
|
FakeIntersectionObserver.instances = []
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers()
|
|
vi.unstubAllGlobals()
|
|
vi.resetModules()
|
|
FakeIntersectionObserver.instances = []
|
|
})
|
|
|
|
describe('markdownRender deferNodesUntilVisible', () => {
|
|
it('keeps nodes as placeholders until IO marks them visible', async () => {
|
|
const OriginalIO = globalThis.IntersectionObserver
|
|
vi.stubGlobal('IntersectionObserver', FakeIntersectionObserver as any)
|
|
|
|
let wrapper: ReturnType<typeof mount> | null = null
|
|
try {
|
|
const MarkdownRender = (await import('../src/components/NodeRenderer')).default
|
|
const markdown = Array.from({ length: 60 }, (_, i) => `Paragraph ${i + 1}`).join('\n\n')
|
|
|
|
wrapper = mount(MarkdownRender, {
|
|
props: {
|
|
content: markdown,
|
|
// Ensure deferral is on and virtualization stays off (60 <= 320 default).
|
|
deferNodesUntilVisible: true,
|
|
viewportPriority: true,
|
|
initialRenderBatchSize: 40,
|
|
},
|
|
})
|
|
|
|
await flushAll()
|
|
|
|
const deferredIndex = 45
|
|
const slot = wrapper.find(`[data-node-index="${deferredIndex}"]`)
|
|
expect(slot.exists()).toBe(true)
|
|
expect(slot.find('.node-placeholder').exists()).toBe(true)
|
|
expect(slot.find('.node-content').exists()).toBe(false)
|
|
|
|
const observer = FakeIntersectionObserver.instances.find(instance => instance.elements.has(slot.element))
|
|
expect(observer).toBeTruthy()
|
|
expect(observer?.elements.has(slot.element)).toBe(true)
|
|
observer?.trigger(slot.element, true)
|
|
expect(observer?.elements.has(slot.element)).toBe(false)
|
|
await flushAll()
|
|
|
|
const visibleSlot = wrapper.find(`[data-node-index="${deferredIndex}"]`)
|
|
expect(visibleSlot.find('.node-placeholder').exists()).toBe(false)
|
|
expect(visibleSlot.find('.node-content').exists()).toBe(true)
|
|
expect(wrapper.text()).toContain(`Paragraph ${deferredIndex + 1}`)
|
|
|
|
await wrapper.setProps({ deferNodesUntilVisible: false })
|
|
await flushAll()
|
|
|
|
const updatedSlot = wrapper.find(`[data-node-index="${deferredIndex}"]`)
|
|
expect(updatedSlot.find('.node-placeholder').exists()).toBe(false)
|
|
expect(updatedSlot.find('.node-content').exists()).toBe(true)
|
|
expect(wrapper.text()).toContain(`Paragraph ${deferredIndex + 1}`)
|
|
}
|
|
finally {
|
|
wrapper?.unmount()
|
|
vi.stubGlobal('IntersectionObserver', OriginalIO as any)
|
|
}
|
|
})
|
|
|
|
it('ignores overflow ancestors that are not actually scrollable when picking the IO root', async () => {
|
|
const OriginalIO = globalThis.IntersectionObserver
|
|
vi.stubGlobal('IntersectionObserver', FakeIntersectionObserver as any)
|
|
|
|
let wrapper: ReturnType<typeof mount> | null = null
|
|
const host = document.createElement('div')
|
|
const fakeOverflow = document.createElement('div')
|
|
host.appendChild(fakeOverflow)
|
|
document.body.appendChild(host)
|
|
fakeOverflow.style.overflow = 'auto'
|
|
|
|
try {
|
|
const MarkdownRender = (await import('../src/components/NodeRenderer')).default
|
|
const markdown = Array.from({ length: 60 }, (_, i) => `Paragraph ${i + 1}`).join('\n\n')
|
|
|
|
wrapper = mount(MarkdownRender, {
|
|
attachTo: fakeOverflow,
|
|
props: {
|
|
content: markdown,
|
|
deferNodesUntilVisible: true,
|
|
viewportPriority: true,
|
|
initialRenderBatchSize: 40,
|
|
},
|
|
})
|
|
|
|
await flushAll()
|
|
|
|
const observer = FakeIntersectionObserver.instances.at(-1)
|
|
expect(observer).toBeTruthy()
|
|
expect(observer?.options.root ?? null).toBe(null)
|
|
}
|
|
finally {
|
|
wrapper?.unmount()
|
|
host.remove()
|
|
vi.stubGlobal('IntersectionObserver', OriginalIO as any)
|
|
}
|
|
})
|
|
})
|