Files
superwork/vscode/webview-ui/src/codeReader/pageLayout.test.ts
T

118 lines
5.1 KiB
TypeScript

// @vitest-environment happy-dom
import type { Placement } from './placement'
import type { BlockBox } from './scrollPosition'
import { afterEach, describe, expect, it } from 'vitest'
import { PageLayout } from './pageLayout'
import { anchorAt, blockForLine } from './scrollPosition'
/** 量过几次位置 */
let reads = 0
/** 元素的位置写死(happy-dom 不排版),每量一次记一次 */
function place<T extends HTMLElement>(el: T, top: number, bottom: number, left = 0, right = 800): T {
el.getBoundingClientRect = () => {
reads++
return { top, bottom, left, right, width: right - left, height: bottom - top, x: left, y: top, toJSON: () => ({}) } as DOMRect
}
return el
}
function div(className: string, attrs: Record<string, string> = {}, ...children: HTMLElement[]): HTMLElement {
const el = document.createElement('div')
el.className = className
for (const [name, value] of Object.entries(attrs))
el.setAttribute(name, value)
el.append(...children)
return el
}
/** 一层:左边竖线那格、右边一栏 */
function level(owner: string | undefined, ...children: HTMLElement[]): HTMLElement {
return div('cr-level', owner === undefined ? {} : { 'data-owner': owner }, div('guide'), div('column', {}, ...children))
}
/** 各块在文件里的起始行(页面上不记行号,按块 id 从文档里各块的位置取) */
const LINES: Record<string, number> = {}
const PLACEMENT: Placement = { of: id => id in LINES ? { offset: 0, line: LINES[id], endLine: LINES[id] } : undefined }
function card(id: string, line: number, top: number, bottom: number, editing = false): HTMLElement {
LINES[id] = line
return place(div('cr-block', { 'data-id': id, ...(editing && { 'data-editing': '' }) }), top, bottom)
}
/**
* 三段顶层块,每段 1000px:第 0 段一张卡片和它的两张子卡片(子层到 900 结束),第 1 段一张卡片(在编辑),第 2 段一张卡片。
*/
function page(): { root: HTMLElement, cards: BlockBox[] } {
const root = document.createElement('div')
root.append(level(
undefined,
place(div('cr-item', {}, card('0', 0, 0, 100), place(level('0', card('0.0', 2, 120, 300), card('0.1', 8, 320, 500)), 110, 900)), 0, 1000),
place(div('cr-item', {}, card('1', 20, 1010, 1500, true)), 1010, 1500),
place(div('cr-item', {}, card('2', 40, 1510, 2000)), 1510, 2000),
))
document.body.append(root)
const cards = [[0, 0, 100], [2, 120, 300], [8, 320, 500], [20, 1010, 1500], [40, 1510, 2000]].map(([line, top, bottom]) => ({ line, top, bottom }))
return { root, cards }
}
afterEach(() => {
document.body.innerHTML = ''
reads = 0
})
describe('卡片位置量过就记着', () => {
it('滚动位置、包含某一行的块:和把全部卡片量一遍算出来的一样', () => {
const { root, cards } = page()
const layout = new PageLayout()
layout.attach(root, null, () => PLACEMENT)
for (const y of [0, 50, 100, 310, 950, 1200, 1999, 2500]) {
const found = layout.anchorAt(y)
expect(found && { line: found.line, offset: found.offset }, `y ${y}`).toEqual((({ line, offset }) => ({ line, offset }))(anchorAt(cards, y)!))
}
for (const line of [0, 1, 5, 9, 25, 100])
expect(layout.cardForLine(line)?.line, `line ${line}`).toBe(blockForLine(cards, line)?.line)
expect(layout.topFor({ line: 8, offset: 5, scrollTop: 9999 })).toBe(325)
expect(layout.topFor({ line: 9, offset: 5, scrollTop: 9999 })).toBe(9999)
})
it('钉住的卡片:只算和顶上那一段相交的顶层块,在编辑的不钉;外层卡片的下沿算到它的子层', () => {
const { root } = page()
const layout = new PageLayout()
layout.attach(root, null, () => PLACEMENT)
window.scrollTo(0, 0)
const scopes = layout.stickyScopes(48)
expect(scopes.map(scope => [scope.id, scope.depth, scope.top, scope.bottom])).toEqual([['0', 0, 0, 900]])
})
it('滚动时不再量:同一份内容问几次都只量一次;只量用到的那几段;内容变了才重量', async () => {
const { root } = page()
const layout = new PageLayout()
layout.attach(root, null, () => PLACEMENT)
layout.anchorAt(0)
// 三段顶层块的外框 + 第 0、1 段里的卡片和子层
const first = reads
expect(first).toBe(3 + 4 + 1)
for (const y of [0, 10, 20, 300, 600])
layout.anchorAt(y)
layout.stickyScopes(48)
expect(reads).toBe(first)
root.querySelector('.cr-item')!.append(div('cr-qa'))
await Promise.resolve()
layout.anchorAt(0)
expect(reads).toBe(first * 2)
})
it('要滚过去的:目标所在的顶层块连同上下几屏之内的先照常排版,过两帧放回去', async () => {
const { root } = page()
const layout = new PageLayout()
layout.attach(root, null, () => PLACEMENT)
const items = [...root.querySelectorAll('.cr-item')]
expect(layout.cardForLine(40)?.line).toBe(40)
expect(items.map(item => item.hasAttribute('data-settle'))).toEqual([true, true, true])
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(() => setTimeout(resolve))))
expect(items.some(item => item.hasAttribute('data-settle'))).toBe(false)
})
})