mirror of
https://github.com/kuaifan/dootask.git
synced 2026-09-14 16:08:35 +00:00
新增到期、排期、逾期与未排期分组,按项目缓存显示选择并至少保留一项。 统一分组复选下拉菜单,接入 Cascader 桌面悬停展开,增加勾选后的滚动定位校正。 升级 view-design-hi 至 4.7.0-81,同步多语言、知识库和回归测试。
111 lines
4.9 KiB
JavaScript
111 lines
4.9 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import {readFileSync} from 'node:fs';
|
|
import test from 'node:test';
|
|
|
|
const source = readFileSync(new URL('../resources/assets/js/utils/scrollToStableTarget.js', import.meta.url), 'utf8');
|
|
|
|
function fixture(render = () => {}) {
|
|
let now = 0;
|
|
let sequence = 0;
|
|
const timers = new Map();
|
|
const events = () => ({
|
|
listeners: new Map(),
|
|
addEventListener(type, fn) {this.listeners.set(type, fn);},
|
|
removeEventListener(type) {this.listeners.delete(type);},
|
|
dispatch(type, event = {}) {this.listeners.get(type)?.(event);},
|
|
});
|
|
const document = events();
|
|
const root = events();
|
|
const scrolls = [];
|
|
const container = {
|
|
ownerDocument: document, isConnected: true, scrollTop: 0, scrollLeft: 48,
|
|
clientTop: 0, clientHeight: 400, scrollHeight: 4000,
|
|
getBoundingClientRect: () => ({top: 20}),
|
|
scrollTo(options) {scrolls.push({...options, at: now}); this.scrollTop = options.top;},
|
|
};
|
|
const target = {isConnected: true, top: 1000, getBoundingClientRect: () => ({top: 20 + target.top - container.scrollTop})};
|
|
const start = new Function('setTimeout', 'clearTimeout', 'Date', `${source.replace('export ', '')}; return scrollToStableTarget;`)(
|
|
(fn, delay) => {const id = ++sequence; timers.set(id, {fn, at: now + delay}); return id;},
|
|
id => timers.delete(id), {now: () => now},
|
|
);
|
|
let live = true;
|
|
const cancel = start({container, target, inputRoot: root, render: () => render({now, container, target}), isActive: () => live});
|
|
const advance = async until => {
|
|
while (timers.size) {
|
|
const [id, timer] = [...timers].sort((a, b) => a[1].at - b[1].at)[0];
|
|
if (timer.at > until) break;
|
|
now = timer.at;
|
|
timers.delete(id);
|
|
timer.fn();
|
|
for (let i = 0; i < 5; i++) await Promise.resolve();
|
|
}
|
|
now = until;
|
|
};
|
|
return {container, target, root, document, scrolls, timers, advance, cancel, invalidate: () => {live = false;}};
|
|
}
|
|
|
|
test('corrects growing and shrinking lazy rows, then stops after stable alignment', async () => {
|
|
const f = fixture(({now, target, container}) => {
|
|
target.top = now < 320 ? 1000 : now < 800 ? 1600 : 1200;
|
|
container.scrollHeight = target.top + 3000;
|
|
});
|
|
await f.advance(3000);
|
|
assert.equal(f.scrolls[0].behavior, 'smooth');
|
|
assert.deepEqual(f.scrolls.slice(1).map(({top, behavior}) => [top, behavior]), [[1600, 'instant'], [1200, 'instant']]);
|
|
assert.equal(f.container.scrollTop, 1200);
|
|
assert.ok(f.scrolls.every(scroll => scroll.left === 48));
|
|
assert.equal(f.timers.size, 0);
|
|
assert.equal(f.root.listeners.size + f.document.listeners.size, 0);
|
|
});
|
|
|
|
test('clamps bottom targets and finishes without fighting the scroll limit', async () => {
|
|
const f = fixture(({target}) => {target.top = 3950;});
|
|
await f.advance(3000);
|
|
assert.equal(f.container.scrollTop, 3600);
|
|
assert.equal(f.timers.size, 0);
|
|
});
|
|
|
|
test('wheel, touch, scrollbar pointer and navigation keys cancel without further corrections', async () => {
|
|
for (const event of ['wheel', 'touchstart', 'pointerdown', 'keydown']) {
|
|
const f = fixture(({target}) => {target.top += 100;});
|
|
await f.advance(160);
|
|
if (event === 'keydown') f.document.dispatch(event, {key: 'PageDown'});
|
|
else f.root.dispatch(event);
|
|
const count = f.scrolls.length;
|
|
assert.equal(f.scrolls.at(-1).behavior, 'instant');
|
|
f.container.scrollTop = 75;
|
|
await f.advance(4000);
|
|
assert.equal(f.scrolls.length, count);
|
|
assert.equal(f.container.scrollTop, 75);
|
|
assert.equal(f.root.listeners.size + f.document.listeners.size, 0);
|
|
}
|
|
});
|
|
|
|
test('hard deadline stops continuous relayout and also a pending render promise', async () => {
|
|
for (const render of [({target}) => {target.top += 10;}, () => new Promise(() => {})]) {
|
|
const f = fixture(render);
|
|
await f.advance(4000);
|
|
assert.equal(f.scrolls.at(-1).at, 2500);
|
|
assert.equal(f.timers.size, 0);
|
|
assert.equal(f.root.listeners.size + f.document.listeners.size, 0);
|
|
}
|
|
});
|
|
|
|
test('replacement, removal and explicit cancellation clean up the old request', async () => {
|
|
for (const invalidate of [f => f.invalidate(), f => {f.target.isConnected = false;}, f => f.cancel()]) {
|
|
const f = fixture();
|
|
invalidate(f);
|
|
await f.advance(3000);
|
|
assert.equal(f.timers.size, 0);
|
|
assert.equal(f.root.listeners.size + f.document.listeners.size, 0);
|
|
assert.equal(f.scrolls.length, 2);
|
|
}
|
|
});
|
|
|
|
test('render errors stop safely and do not leave timers or listeners', async () => {
|
|
const f = fixture(() => {throw new Error('unmounted');});
|
|
await f.advance(3000);
|
|
assert.equal(f.timers.size, 0);
|
|
assert.equal(f.root.listeners.size + f.document.listeners.size, 0);
|
|
});
|