- 审计 15 个日志/事件面后统一口径:改 3 个(渠道总日志 / 每账号消息日志弹窗 / 适配器专属页实时日志),7 个本已合规未动,聊天页**刻意保持正序**(本质是读对话,正序+自动滚底才自然)
- 排序口径唯一出处 channelLogOrder.ts(历史+WS 追帧按 id 去重、时间倒序)+ 展示层不倒序(与 channelEventViews 同分工);三面共用 useChannelMessageLog 取数
- 新增 logScroll.ts:贴顶跟着最新走、已下读按新增高度补偿(**新条目不把正在看的内容顶跑**);窗口化容器的滚动锚定一并接上
- 后端无需改(三处消息日志读取本就是 created_at DESC,已核)
- 诚实边界:滚动锚定的 DOM 那半只有纯函数有自动化证据(项目无 jsdom/Playwright,渲染测试跑 react-dom/server、effect 不执行)⇒ 已如实标注
- 变异实测:排序改回旧→新 / 取消补偿 / 渲染层再倒一次 / 改坏共享文案 —— 四处均打红
- ⚠️ 两处未改(在别的切片文件面内,已转交):flow/NodeDetail 节点实时日志 → 归 F2;Agents 任务日志 → 归 H1
100 lines
4.8 KiB
TypeScript
100 lines
4.8 KiB
TypeScript
// 「最新在上」日志面的滚动锚定逻辑测试(L1「日志界面倒序统一」)。
|
||
//
|
||
// 锁两件事(都是主人报过痛的那一类):
|
||
// ① 顶部前插新条目时,**已经往下读**的用户不能被顶跑 —— scrollTop 必须按新增高度
|
||
// 补偿,正在看的那几条留在原地;
|
||
// ② 贴在顶部的用户要跟着最新一条走(保持贴顶),不能停在半路。
|
||
// 顺序与滚动在浏览器里「画出来才算数」,但位置数学是纯函数:项目没有前端测试框架,
|
||
// 这里用零依赖的 plain-node 检查直接钉住它。
|
||
//
|
||
// 跑法(用 vite 自带的 esbuild,不新增依赖、不改 package.json):
|
||
//
|
||
// cd frontend/web \
|
||
// && ./node_modules/.bin/esbuild src/admin/logScroll.test.ts \
|
||
// --bundle --platform=node --format=esm \
|
||
// --outfile=/tmp/cyrene-log-scroll.test.mjs \
|
||
// && node /tmp/cyrene-log-scroll.test.mjs
|
||
//
|
||
// `tsc --noEmit`(npm run build 的一部分)会对本文件做类型检查,因此它
|
||
// 只从项目内导入、不 import node:*。
|
||
|
||
import { LOG_TOP_EPSILON, initialLogScroll, isAtLogTop, topAnchoredScrollTop } from "./logScroll";
|
||
|
||
let checks = 0;
|
||
|
||
function ok(cond: boolean, what: string): void {
|
||
checks += 1;
|
||
if (!cond) throw new Error(`FAILED: ${what}`);
|
||
}
|
||
|
||
function same(got: number, want: number, what: string): void {
|
||
ok(got === want, `${what} — got ${got}, want ${want}`);
|
||
}
|
||
|
||
// ── ① 「贴着顶部」的判定(容差内算贴顶)────────────────────────────────────────
|
||
ok(initialLogScroll().atTop, "初始快照按「贴着顶部」处理(首屏不该被当成在读旧内容)");
|
||
same(initialLogScroll().scrollTop, 0, "初始滚动位置为 0");
|
||
same(initialLogScroll().contentHeight, 0, "初始内容高度为 0(还没测量)");
|
||
ok(isAtLogTop(0), "scrollTop=0 是贴顶");
|
||
ok(isAtLogTop(LOG_TOP_EPSILON), "容差边缘(=LOG_TOP_EPSILON)仍算贴顶");
|
||
ok(!isAtLogTop(LOG_TOP_EPSILON + 1), "超出容差就不算贴顶");
|
||
|
||
// ── ② 贴顶用户:新条目落在顶部 ⇒ 保持贴顶(最新一条立刻可见)──────────────────
|
||
same(
|
||
topAnchoredScrollTop({ scrollTop: 0, contentHeight: 1000, atTop: true }, 1180),
|
||
0,
|
||
"贴顶时保持 0:新条目就在眼前,不该被挪到半路",
|
||
);
|
||
same(
|
||
topAnchoredScrollTop({ scrollTop: 3, contentHeight: 1000, atTop: true }, 900),
|
||
0,
|
||
"贴顶且内容反而变短时也回到顶部(不留在悬空位置)",
|
||
);
|
||
|
||
// ── ③ 已经往下读的用户:按新增高度补偿 ⇒ 阅读位置不动 ─────────────────────────
|
||
same(
|
||
topAnchoredScrollTop({ scrollTop: 400, contentHeight: 1000, atTop: false }, 1180),
|
||
580,
|
||
"顶部前插 180px ⇒ scrollTop 加 180,正在看的内容留在原地",
|
||
);
|
||
same(
|
||
topAnchoredScrollTop({ scrollTop: 400, contentHeight: 1000, atTop: false }, 1000),
|
||
400,
|
||
"没有新条目(高度没变)⇒ 位置一点不动",
|
||
);
|
||
same(
|
||
topAnchoredScrollTop({ scrollTop: 400, contentHeight: 1000, atTop: false }, 820),
|
||
400,
|
||
"内容变短时不补偿:不编造一个不存在的位移",
|
||
);
|
||
|
||
// ── ④ 端到端语义:被阅读的那一行在补偿前后都停在视口同样的位置 ────────────────
|
||
// 先把「用户停在 scrollTop=300」这件事换算成「某一行距列表顶 300px」;顶部前插 3 行
|
||
// (每行 76px = 228px)后,这一行跑到 528px —— 补偿后它必须仍然正好在视口顶。
|
||
const anchored = topAnchoredScrollTop({ scrollTop: 300, contentHeight: 5000, atTop: false }, 5228);
|
||
same(anchored, 528, "补偿后 scrollTop 等于该行的新绝对位置(视口顶仍是同一行)");
|
||
let scrollTop = 300;
|
||
let contentHeight = 5000;
|
||
for (let frame = 0; frame < 3; frame += 1) {
|
||
const grew = 76;
|
||
scrollTop = topAnchoredScrollTop({ scrollTop, contentHeight, atTop: false }, contentHeight + grew);
|
||
contentHeight += grew;
|
||
}
|
||
same(scrollTop, 300 + 3 * 76, "连来三帧(每次前插 1 行)后累计补偿 3 行高度");
|
||
ok(scrollTop >= 0, "补偿永远不会把 scrollTop 推到负数");
|
||
|
||
// ── ⑤ 贴顶 / 已下读两种用户的对照:同一份新数据,两种正确的行为 ────────────────
|
||
const frame = { contentHeight: 1000 } as const;
|
||
same(
|
||
topAnchoredScrollTop({ scrollTop: 0, contentHeight: frame.contentHeight, atTop: true }, 1240),
|
||
0,
|
||
"贴顶用户:跟住最新一条",
|
||
);
|
||
same(
|
||
topAnchoredScrollTop({ scrollTop: 600, contentHeight: frame.contentHeight, atTop: false }, 1240),
|
||
840,
|
||
"已下读用户:留在原来那几条上",
|
||
);
|
||
|
||
console.log(`log scroll test: ${checks} checks passed(初始快照 + 贴顶判定 6 + 贴顶行为 2 + 下读补偿 3 + 连续帧累计 3 + 两态对照 2)`);
|