Files
ttstd-infinite-canvas/web/test/director-transaction-lifecycle.test.ts

233 lines
8.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, test } from "bun:test";
import { createPrevisTransaction, installPrevisTerminalListeners, type PrevisListenerTarget } from "../src/lib/canvas/previs/previs-gesture-transaction";
type Recorded = { commits: Array<[number, number]>; restores: number[]; active: boolean[]; terminated: number };
function harness(initial = 0) {
const log: Recorded = { commits: [], restores: [], active: [], terminated: 0 };
let value = initial;
const transaction = createPrevisTransaction<number>({
read: () => value,
restore: (snapshot) => {
value = snapshot;
log.restores.push(snapshot);
},
commit: (from, to) => log.commits.push([from, to]),
setActive: (next) => log.active.push(next),
terminateDrag: () => {
log.terminated += 1;
},
});
return {
log,
transaction,
set: (next: number) => {
value = next;
},
get: () => value,
};
}
function fakeTarget() {
const listeners = new Map<string, Array<(event: Event) => void>>();
const target: PrevisListenerTarget = {
addEventListener: (type, listener) => {
listeners.set(type, [...(listeners.get(type) || []), listener]);
},
removeEventListener: (type, listener) => {
listeners.set(
type,
(listeners.get(type) || []).filter((item) => item !== listener),
);
},
};
return { target, emit: (type: string, event: Partial<Event> = {}) => (listeners.get(type) || []).forEach((listener) => listener(event as Event)), count: () => [...listeners.values()].reduce((sum, items) => sum + items.length, 0) };
}
describe("终止监听的安装生命周期(#1 回归)", () => {
test("监听先安装、手势后开始,终止事件仍然生效", () => {
const { log, transaction, set } = harness(5);
const win = fakeTarget();
const doc = fakeTarget();
// 关键顺序:先安装监听,此时并没有活跃手势。
installPrevisTerminalListeners(transaction, { window: win.target, document: doc.target, isHidden: () => false });
expect(win.count() + doc.count()).toBeGreaterThan(0);
transaction.begin();
set(9);
win.emit("pointerup");
expect(log.commits).toEqual([[5, 9]]);
});
test("Escape 取消并恢复快照", () => {
const { log, transaction, set, get } = harness(1);
const win = fakeTarget();
const doc = fakeTarget();
installPrevisTerminalListeners(transaction, { window: win.target, document: doc.target, isHidden: () => false });
transaction.begin();
set(42);
win.emit("keydown", { key: "Escape" } as unknown as Event);
expect(log.commits).toEqual([]);
expect(get()).toBe(1);
expect(log.terminated).toBe(1);
});
test("window blur 保留当前可见值并只 commit 一次(#305 验收:失焦不回原点)", () => {
const { log, transaction, set, get } = harness(2);
const win = fakeTarget();
const doc = fakeTarget();
installPrevisTerminalListeners(transaction, { window: win.target, document: doc.target, isHidden: () => false });
transaction.begin();
set(77);
win.emit("blur");
expect(log.commits).toEqual([[2, 77]]);
expect(get()).toBe(77);
expect(log.restores).toEqual([]);
// 失焦后再来终态事件不得二次提交。
win.emit("blur");
win.emit("pointerup");
expect(log.commits).toEqual([[2, 77]]);
});
test("document hidden 保留当前可见值并只 commit 一次", () => {
const { log, transaction, set, get } = harness(3);
const win = fakeTarget();
const doc = fakeTarget();
installPrevisTerminalListeners(transaction, { window: win.target, document: doc.target, isHidden: () => true });
transaction.begin();
set(88);
doc.emit("visibilitychange");
expect(log.commits).toEqual([[3, 88]]);
expect(get()).toBe(88);
expect(log.restores).toEqual([]);
doc.emit("visibilitychange");
expect(log.commits).toEqual([[3, 88]]);
});
test("pointercancel 仍然取消并恢复快照", () => {
const { log, transaction, set, get } = harness(2);
const win = fakeTarget();
const doc = fakeTarget();
installPrevisTerminalListeners(transaction, { window: win.target, document: doc.target, isHidden: () => false });
transaction.begin();
set(77);
win.emit("pointercancel");
expect(log.commits).toEqual([]);
expect(get()).toBe(2);
expect(log.restores).toEqual([2]);
});
test("可见性事件在未隐藏时既不提交也不取消", () => {
const { log, transaction, set } = harness(4);
const win = fakeTarget();
const doc = fakeTarget();
installPrevisTerminalListeners(transaction, { window: win.target, document: doc.target, isHidden: () => false });
transaction.begin();
set(6);
doc.emit("visibilitychange");
expect(transaction.active()).toBe(true);
expect(log.commits).toEqual([]);
win.emit("pointerup");
expect(log.commits).toEqual([[4, 6]]);
});
test("disposer 摘除全部监听", () => {
const { log, transaction, set } = harness(0);
const win = fakeTarget();
const doc = fakeTarget();
const dispose = installPrevisTerminalListeners(transaction, { window: win.target, document: doc.target, isHidden: () => false });
dispose();
expect(win.count() + doc.count()).toBe(0);
transaction.begin();
set(3);
win.emit("pointerup");
expect(log.commits).toEqual([]);
});
});
describe("终态幂等与陈旧 base 防护(#4 回归)", () => {
test("正常结束恰好提交一次,重复终止不再提交", () => {
const { log, transaction, set } = harness(1);
transaction.begin();
set(2);
transaction.end("commit");
transaction.end("commit");
transaction.end("cancel");
expect(log.commits).toEqual([[1, 2]]);
expect(log.restores).toEqual([]);
expect(log.active).toEqual([true, false]);
});
test("取消后不提交,且不会残留可被后续消费的 base", () => {
const { log, transaction, set, get } = harness(10);
transaction.begin();
set(20);
transaction.end("cancel");
expect(get()).toBe(10);
// 取消后的 end 不能拿旧 base 再提交一次。
transaction.end("commit");
expect(log.commits).toEqual([]);
expect(transaction.active()).toBe(false);
});
test("未开始手势时的终止是空操作,不会误提交普通点击", () => {
const { log, transaction } = harness(7);
transaction.end("commit");
transaction.end("cancel");
expect(log.commits).toEqual([]);
expect(log.restores).toEqual([]);
expect(log.active).toEqual([]);
});
test("终态后可以重新开始新手势,base 取新快照而非旧值", () => {
const { log, transaction, set } = harness(1);
transaction.begin();
set(2);
transaction.end("commit");
set(50);
transaction.begin();
set(60);
transaction.end("commit");
expect(log.commits).toEqual([
[1, 2],
[50, 60],
]);
});
test("取消后新手势的 base 是恢复后的值", () => {
const { log, transaction, set } = harness(5);
transaction.begin();
set(99);
transaction.end("cancel");
transaction.begin();
set(8);
transaction.end("commit");
expect(log.commits).toEqual([[5, 8]]);
});
test("read 返回 null 时不进入手势,终止也不提交", () => {
const log: Array<[number, number]> = [];
const transaction = createPrevisTransaction<number>({
read: () => null,
restore: () => undefined,
commit: (from, to) => log.push([from, to]),
setActive: () => undefined,
});
transaction.begin();
expect(transaction.active()).toBe(false);
transaction.end("commit");
expect(log).toEqual([]);
});
test("提交路径不触发第三方拖拽终止,取消路径才触发", () => {
const committed = harness(0);
committed.transaction.begin();
committed.transaction.end("commit");
expect(committed.log.terminated).toBe(0);
const cancelled = harness(0);
cancelled.transaction.begin();
cancelled.transaction.end("cancel");
expect(cancelled.log.terminated).toBe(1);
});
});