118 lines
5.1 KiB
TypeScript
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)
|
|
})
|
|
})
|