Why: The pane pinned .ui-fp to the column height, and the docx fit host then clipped overflow, so the right-hand preview could not scroll. The fullscreen DocViewer was a second shell around the same FilePreview. Evidence: npx vitest run (17 files, 82 tests) and npx tsc --noEmit passed. Headless Chromium laid out a 1400px docx host inside a 764px pane (body scrollHeight 1400) and a pdf iframe that filled that pane. Impact: .preview-pane-body is the scroll owner. Docx refit also watches page height. The overlay viewer is gone; expand/restore and text edit/save live on the pane. Refs: Agent-System-Dev#196, Agent-System-Frontend#44 Co-authored-by: Cursor <cursoragent@cursor.com>
111 lines
3.7 KiB
TypeScript
111 lines
3.7 KiB
TypeScript
import { afterEach, describe, it, expect } from 'vitest'
|
|
import { computeFitScale, fitPagesToWidth } from '../src/previewfit'
|
|
|
|
describe('computeFitScale', () => {
|
|
it('keeps the page at 100% when the column is wider than the page', () => {
|
|
expect(computeFitScale(1100, 794, 24)).toBe(1)
|
|
expect(computeFitScale(818, 794, 24)).toBe(1)
|
|
})
|
|
|
|
it('scales the page down to the column width after subtracting the gutter', () => {
|
|
expect(computeFitScale(420, 794, 24)).toBeCloseTo((420 - 24) / 794)
|
|
expect(computeFitScale(794, 794, 24)).toBeCloseTo((794 - 24) / 794)
|
|
})
|
|
|
|
it('does not invert or zoom when the width is unusable', () => {
|
|
expect(computeFitScale(0, 794, 24)).toBe(1)
|
|
expect(computeFitScale(20, 794, 24)).toBe(1)
|
|
expect(computeFitScale(420, 0, 24)).toBe(1)
|
|
expect(computeFitScale(Number.NaN, 794, 24)).toBe(1)
|
|
})
|
|
})
|
|
|
|
// node 测试环境没有 DOM。这里只实现 fitPagesToWidth 会碰到的查询与尺寸。
|
|
class FakeEl {
|
|
style: { height: string; overflow: string; minHeight: string; transform: string; transformOrigin: string } = {
|
|
height: '', overflow: '', minHeight: '', transform: '', transformOrigin: '',
|
|
}
|
|
clientWidth = 0
|
|
offsetWidth = 0
|
|
offsetHeight = 0
|
|
className = ''
|
|
tag = 'div'
|
|
children: FakeEl[] = []
|
|
querySelector(sel: string): FakeEl | null {
|
|
const walk = (nodes: FakeEl[]): FakeEl | null => {
|
|
for (const node of nodes) {
|
|
if (matches(node, sel)) return node
|
|
const hit = walk(node.children)
|
|
if (hit) return hit
|
|
}
|
|
return null
|
|
}
|
|
return walk(this.children)
|
|
}
|
|
}
|
|
|
|
function matches(el: FakeEl, sel: string): boolean {
|
|
const classes = el.className.split(/\s+/).filter(Boolean)
|
|
if (sel.startsWith('.')) return classes.includes(sel.slice(1))
|
|
const [tag, cls] = sel.split('.')
|
|
return el.tag === tag && classes.includes(cls)
|
|
}
|
|
|
|
class FakeResizeObserver {
|
|
static observed: FakeEl[] = []
|
|
static instances: FakeResizeObserver[] = []
|
|
constructor(private callback: () => void) {
|
|
FakeResizeObserver.instances.push(this)
|
|
}
|
|
observe(el: FakeEl) { FakeResizeObserver.observed.push(el) }
|
|
disconnect() { /* dispose 只要求卸下监听 */ }
|
|
static fire() {
|
|
for (const instance of FakeResizeObserver.instances) instance.callback()
|
|
}
|
|
static reset() {
|
|
FakeResizeObserver.observed = []
|
|
FakeResizeObserver.instances = []
|
|
}
|
|
}
|
|
|
|
describe('fitPagesToWidth', () => {
|
|
const previous = globalThis.ResizeObserver
|
|
|
|
afterEach(() => {
|
|
FakeResizeObserver.reset()
|
|
globalThis.ResizeObserver = previous
|
|
})
|
|
|
|
it('recomputes the host height when the page wrapper grows and clears inline styles on dispose', () => {
|
|
globalThis.ResizeObserver = FakeResizeObserver as unknown as typeof ResizeObserver
|
|
const host = new FakeEl()
|
|
host.clientWidth = 420
|
|
const wrapper = new FakeEl()
|
|
wrapper.className = 'docx-wrapper'
|
|
wrapper.offsetHeight = 1000
|
|
const page = new FakeEl()
|
|
page.tag = 'section'
|
|
page.className = 'docx'
|
|
page.offsetWidth = 794
|
|
wrapper.children.push(page)
|
|
host.children.push(wrapper)
|
|
|
|
const dispose = fitPagesToWidth(host as unknown as HTMLElement, host as unknown as HTMLElement)
|
|
const scale = (420 - 24) / 794
|
|
expect(host.style.height).toBe(`${Math.round(1000 * scale)}px`)
|
|
expect(host.style.overflow).toBe('hidden')
|
|
expect(FakeResizeObserver.observed).toContain(wrapper)
|
|
|
|
wrapper.offsetHeight = 1500
|
|
FakeResizeObserver.fire()
|
|
expect(host.style.height).toBe(`${Math.round(1500 * scale)}px`)
|
|
|
|
dispose()
|
|
expect(host.style.height).toBe('')
|
|
expect(host.style.overflow).toBe('')
|
|
expect(host.style.minHeight).toBe('')
|
|
expect(wrapper.style.transform).toBe('')
|
|
expect(wrapper.style.transformOrigin).toBe('')
|
|
})
|
|
})
|