dsh-activity-pane 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dsh-plugin/client.js +183 -41
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/package.json +1 -1
- package/scripts/acceptance.mjs +9 -5
- package/scripts/bench.mjs +2 -2
- package/scripts/check.mjs +146 -27
- package/src/client.mjs +132 -22
- package/src/core.mjs +51 -19
package/scripts/check.mjs
CHANGED
|
@@ -33,7 +33,9 @@ import {
|
|
|
33
33
|
openTurnStartMissing,
|
|
34
34
|
escapeCssString,
|
|
35
35
|
firstPhysicalLine,
|
|
36
|
+
fmtAbsoluteDateTime,
|
|
36
37
|
fmtElapsedMs,
|
|
38
|
+
fmtRelativeAge,
|
|
37
39
|
fmtTokens,
|
|
38
40
|
isActiveRow,
|
|
39
41
|
shouldSubscribeToSession,
|
|
@@ -754,11 +756,11 @@ const drainGapIdle = {
|
|
|
754
756
|
current: null,
|
|
755
757
|
};
|
|
756
758
|
assert.equal(
|
|
757
|
-
buildRecent(drainGapIdle, [], 2000,
|
|
759
|
+
buildRecent(drainGapIdle, [], 2000, {}, [], null, new Set(["root"])).length,
|
|
758
760
|
0,
|
|
759
761
|
"耗尽空窗内委托周期会话不入最近历史(R-01-010 分区不变量)",
|
|
760
762
|
);
|
|
761
|
-
assert.equal(buildRecent(drainGapIdle, [], 2000,
|
|
763
|
+
assert.equal(buildRecent(drainGapIdle, [], 2000, {}, [], null).length, 1, "委托周期结束后才入最近历史");
|
|
762
764
|
// delegationActive:耗尽宽限内视为委托周期(空窗保持),超时退出。
|
|
763
765
|
assert.equal(delegationActive({ mode: "delegating", anchor: 1000, turnStart: 9000, drainedAt: null }, 50000), true, "委托周期中视为活动");
|
|
764
766
|
assert.equal(delegationActive({ mode: "delegating", anchor: 1000, turnStart: 9000, drainedAt: 30000 }, 31000), true, "耗尽宽限内视为活动(空窗保持运行呈现)");
|
|
@@ -897,6 +899,35 @@ assert.equal(fmtElapsedMs(193_000), "3m13s", "时长分秒格式");
|
|
|
897
899
|
assert.equal(fmtElapsedMs(NaN), "", "NaN 时长归一为空");
|
|
898
900
|
assert.equal(fmtElapsedMs(Infinity), "", "Infinity 时长归一为空");
|
|
899
901
|
assert.equal(fmtElapsedMs(-1), "", "负时长归一为空");
|
|
902
|
+
// R-01-013/AC-05:历史卡同时显示本地绝对日期时间与相对年龄,异常输入不制造虚假时间。
|
|
903
|
+
const relativeMinute = 60_000;
|
|
904
|
+
const relativeHour = 60 * relativeMinute;
|
|
905
|
+
const relativeDay = 24 * relativeHour;
|
|
906
|
+
assert.equal(fmtRelativeAge(0), "刚刚", "小于一分钟显示刚刚");
|
|
907
|
+
assert.equal(fmtRelativeAge(2 * relativeMinute + 1), "2分钟前", "分钟级相对时间");
|
|
908
|
+
assert.equal(fmtRelativeAge(3 * relativeHour + 1), "3小时前", "小时级相对时间");
|
|
909
|
+
assert.equal(fmtRelativeAge(2 * relativeDay + 1), "2天前", "天级相对时间");
|
|
910
|
+
assert.equal(fmtRelativeAge(2 * 7 * relativeDay + 1), "2周前", "周级相对时间");
|
|
911
|
+
assert.equal(fmtRelativeAge(2 * 30 * relativeDay + 1), "2个月前", "月级相对时间");
|
|
912
|
+
assert.equal(fmtRelativeAge(2 * 365 * relativeDay + 1), "2年前", "年级相对时间");
|
|
913
|
+
assert.equal(fmtRelativeAge(NaN), "", "NaN 相对时间归一为空");
|
|
914
|
+
assert.equal(fmtRelativeAge(Infinity), "", "Infinity 相对时间归一为空");
|
|
915
|
+
assert.equal(fmtRelativeAge(-1), "", "负相对时间归一为空");
|
|
916
|
+
const sameYearActivity = new Date(2026, 7, 31, 14, 32);
|
|
917
|
+
const sameYearNow = new Date(2026, 8, 1, 9, 0);
|
|
918
|
+
assert.equal(
|
|
919
|
+
fmtAbsoluteDateTime(sameYearActivity.getTime(), sameYearNow.getTime()),
|
|
920
|
+
`${sameYearActivity.toLocaleDateString([], { month: "2-digit", day: "2-digit" })} ${sameYearActivity.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}`,
|
|
921
|
+
"当前年份绝对时间省略年份但保留月日时分",
|
|
922
|
+
);
|
|
923
|
+
const priorYearActivity = new Date(2025, 11, 31, 23, 50);
|
|
924
|
+
assert.equal(
|
|
925
|
+
fmtAbsoluteDateTime(priorYearActivity.getTime(), sameYearNow.getTime()),
|
|
926
|
+
`${priorYearActivity.toLocaleDateString([], { year: "numeric", month: "2-digit", day: "2-digit" })} ${priorYearActivity.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}`,
|
|
927
|
+
"跨年份绝对时间补充年份",
|
|
928
|
+
);
|
|
929
|
+
assert.equal(fmtAbsoluteDateTime(null, sameYearNow.getTime()), "", "缺失绝对时间归一为空");
|
|
930
|
+
assert.equal(fmtAbsoluteDateTime(NaN, sameYearNow.getTime()), "", "非法绝对时间归一为空");
|
|
900
931
|
|
|
901
932
|
// R-01-012/AC-01
|
|
902
933
|
// R-01-012/AC-02
|
|
@@ -2069,7 +2100,8 @@ assert.notEqual(
|
|
|
2069
2100
|
cardSignature(entries),
|
|
2070
2101
|
"缓存命中率变化签名必变",
|
|
2071
2102
|
);
|
|
2072
|
-
// ---- R-01-010/AC-01
|
|
2103
|
+
// ---- R-01-010/AC-01 非活动主会话→最近历史区(不设时间窗口) ----
|
|
2104
|
+
const RECENT_PAGE_SIZE = 10;
|
|
2073
2105
|
const NOW = 2_000_000_000_000; // 固定时钟便于确定性断言
|
|
2074
2106
|
// 注意:completed:true 的主会话是"待打开"的活动卡(在活动区),不属历史区;
|
|
2075
2107
|
// 真实"最近历史"是已处理(running:false 且无未确认完成)的非活动会话;
|
|
@@ -2090,19 +2122,19 @@ const recentSnap = {
|
|
|
2090
2122
|
const recentUncompleted = buildRecent(recentSnap, [], NOW);
|
|
2091
2123
|
assert.deepEqual(
|
|
2092
2124
|
recentUncompleted.map((e) => e.id),
|
|
2093
|
-
["sAwait", "sB"],
|
|
2094
|
-
"completed
|
|
2125
|
+
["sAwait", "sB", "sOld"],
|
|
2126
|
+
"completed 边沿标志不参与活动/历史判定;无完成登记时所有非活动主会话均入历史区(C-030)",
|
|
2095
2127
|
);
|
|
2096
2128
|
// 完成登记存在时(未确认完成):待开 E 显示为完成提醒并排除出历史区(AC-03、R-01-010/AC-06)。
|
|
2097
2129
|
const recentAcks = new Map([
|
|
2098
2130
|
["sAwait", { lastTurnEnd: NOW - 1_000, ackedAt: null }],
|
|
2099
2131
|
["sAwait2", { lastTurnEnd: NOW - 900, ackedAt: NOW - 2_000 }],
|
|
2100
2132
|
]);
|
|
2101
|
-
const recent = buildRecent(recentSnap, [], NOW,
|
|
2133
|
+
const recent = buildRecent(recentSnap, [], NOW, {}, [], recentAcks);
|
|
2102
2134
|
assert.deepEqual(
|
|
2103
2135
|
recent.map((e) => e.id),
|
|
2104
|
-
["sB"],
|
|
2105
|
-
"
|
|
2136
|
+
["sB", "sOld"],
|
|
2137
|
+
"完成登记表中未确认完成的会话留在活动区、排除出历史区;已确认的不再排除",
|
|
2106
2138
|
);
|
|
2107
2139
|
assert.deepEqual(
|
|
2108
2140
|
[recent[0].model, recent[0].reasoning, recent[0].userPreview, recent[0].agentPreview],
|
|
@@ -2117,7 +2149,7 @@ assert.deepEqual(messagePreviews({ history: [] }), { userPreview: "", agentPrevi
|
|
|
2117
2149
|
// R-01-013/AC-04
|
|
2118
2150
|
// R-01-013/AC-05
|
|
2119
2151
|
// R-01-013/AC-06
|
|
2120
|
-
const recentWithPreviews = buildRecent(recentSnap, [], NOW,
|
|
2152
|
+
const recentWithPreviews = buildRecent(recentSnap, [], NOW, {
|
|
2121
2153
|
sB: {
|
|
2122
2154
|
model: { model: "Model M", reasoning: "High" },
|
|
2123
2155
|
previews: { userPreview: "用户首行", agentPreview: "回复首行" },
|
|
@@ -2169,7 +2201,7 @@ const viaRunToIdle = buildEntries(
|
|
|
2169
2201
|
[],
|
|
2170
2202
|
);
|
|
2171
2203
|
assert.deepEqual(viaRunToIdle.map((e) => e.id), ["sB"], "运行中会话在活动区");
|
|
2172
|
-
const recentAfterIdle = buildRecent(recentSnap, [], NOW,
|
|
2204
|
+
const recentAfterIdle = buildRecent(recentSnap, [], NOW, {}, [], recentAcks);
|
|
2173
2205
|
assert.ok(recentAfterIdle.some((e) => e.id === "sB"), "转为非活动后进入历史区");
|
|
2174
2206
|
|
|
2175
2207
|
// ---- R-01-010/AC-03 历史区按最后活动时间从新到旧 ----
|
|
@@ -2192,6 +2224,52 @@ assert.deepEqual(
|
|
|
2192
2224
|
"历史区按最后活动时间倒序",
|
|
2193
2225
|
);
|
|
2194
2226
|
|
|
2227
|
+
// ---- R-01-019/AC-01~AC-03 历史候选不再截断,按 10 条分批呈现 ----
|
|
2228
|
+
const manyRecentIds = Array.from({ length: RECENT_PAGE_SIZE * 2 + 1 }, (_, index) => `page-${index}`);
|
|
2229
|
+
const manyRecent = buildRecent(
|
|
2230
|
+
{
|
|
2231
|
+
ids: manyRecentIds,
|
|
2232
|
+
byId: Object.fromEntries(manyRecentIds.map((id, index) => [id, {
|
|
2233
|
+
id,
|
|
2234
|
+
displayTitle: id,
|
|
2235
|
+
running: false,
|
|
2236
|
+
updatedAt: NOW - index,
|
|
2237
|
+
}])),
|
|
2238
|
+
current: null,
|
|
2239
|
+
},
|
|
2240
|
+
[],
|
|
2241
|
+
NOW,
|
|
2242
|
+
);
|
|
2243
|
+
assert.equal(manyRecent.length, manyRecentIds.length, "历史候选超过 20 条时不再被核心截断(R-01-019/AC-01)");
|
|
2244
|
+
assert.deepEqual(
|
|
2245
|
+
manyRecent.slice(0, RECENT_PAGE_SIZE).map((entry) => entry.id),
|
|
2246
|
+
manyRecentIds.slice(0, RECENT_PAGE_SIZE),
|
|
2247
|
+
"历史首批按最近顺序取 10 条(R-01-019/AC-01)",
|
|
2248
|
+
);
|
|
2249
|
+
assert.deepEqual(
|
|
2250
|
+
manyRecent.slice(RECENT_PAGE_SIZE, RECENT_PAGE_SIZE * 2).map((entry) => entry.id),
|
|
2251
|
+
manyRecentIds.slice(RECENT_PAGE_SIZE, RECENT_PAGE_SIZE * 2),
|
|
2252
|
+
"历史第二批紧接首批取后续 10 条(R-01-019/AC-02)",
|
|
2253
|
+
);
|
|
2254
|
+
assert.deepEqual(
|
|
2255
|
+
manyRecent.slice(RECENT_PAGE_SIZE * 2).map((entry) => entry.id),
|
|
2256
|
+
manyRecentIds.slice(RECENT_PAGE_SIZE * 2),
|
|
2257
|
+
"历史末批只取剩余会话且不会越界重复(R-01-019/AC-03)",
|
|
2258
|
+
);
|
|
2259
|
+
const tiedRecent = buildRecent(
|
|
2260
|
+
{
|
|
2261
|
+
ids: ["tie-z", "tie-a"],
|
|
2262
|
+
byId: {
|
|
2263
|
+
"tie-z": { id: "tie-z", displayTitle: "Z", running: false, updatedAt: NOW },
|
|
2264
|
+
"tie-a": { id: "tie-a", displayTitle: "A", running: false, updatedAt: NOW },
|
|
2265
|
+
},
|
|
2266
|
+
current: null,
|
|
2267
|
+
},
|
|
2268
|
+
[],
|
|
2269
|
+
NOW,
|
|
2270
|
+
);
|
|
2271
|
+
assert.deepEqual(tiedRecent.map((entry) => entry.id), ["tie-a", "tie-z"], "相同 activityAt 时以会话 id 稳定排序");
|
|
2272
|
+
|
|
2195
2273
|
// ---- R-01-010/AC-08 最后活动时间:turn/end 提取与 max 归一 ----
|
|
2196
2274
|
assert.equal(lastTurnEndFromEvents([]), null, "空 history 无回合结束时刻");
|
|
2197
2275
|
assert.equal(
|
|
@@ -2326,7 +2404,7 @@ const refineSnap = {
|
|
|
2326
2404
|
},
|
|
2327
2405
|
current: null,
|
|
2328
2406
|
};
|
|
2329
|
-
const refined = buildRecent(refineSnap, [], NOW,
|
|
2407
|
+
const refined = buildRecent(refineSnap, [], NOW, {}, [], null, null, {
|
|
2330
2408
|
sTurn: NOW - 2_000, // 回合结束晚于宿主时间 → 精化为回合结束时刻
|
|
2331
2409
|
sPrompt: NOW - 3_000, // 回合结束早于宿主时间(消息未处理)→ 取较新者
|
|
2332
2410
|
});
|
|
@@ -2343,19 +2421,18 @@ assert.deepEqual(
|
|
|
2343
2421
|
[["sPrompt", NOW - 1_000], ["sNone", NOW - 5_000], ["sTurn", NOW - 10_000]],
|
|
2344
2422
|
"回合结束时刻在途时先按宿主列表时间判定、排序与显示(R-01-010/AC-09)",
|
|
2345
2423
|
);
|
|
2346
|
-
//
|
|
2424
|
+
// 无时间窗口语义:宿主时间较早的非活动会话仍进入历史区,已知 turn/end 继续精化排序(C-068)。
|
|
2347
2425
|
const crossWindow = buildRecent(
|
|
2348
2426
|
{ ids: ["sLong"], byId: { sLong: { id: "sLong", displayTitle: "长回合", running: false, updatedAt: NOW - 25 * 3_600_000 } }, current: null },
|
|
2349
2427
|
[],
|
|
2350
2428
|
NOW,
|
|
2351
|
-
undefined,
|
|
2352
2429
|
{},
|
|
2353
2430
|
[],
|
|
2354
2431
|
null,
|
|
2355
2432
|
null,
|
|
2356
2433
|
{ sLong: NOW - 1_000 },
|
|
2357
2434
|
);
|
|
2358
|
-
assert.
|
|
2435
|
+
assert.deepEqual(crossWindow.map((entry) => [entry.id, entry.activityAt]), [["sLong", NOW - 1_000]], "较早宿主时间的非活动会话仍进入历史区(C-068)");
|
|
2359
2436
|
|
|
2360
2437
|
// ---- R-01-002/AC-03、AC-05、AC-10~AC-12、R-01-010/AC-06 完成确认:未确认完成提醒保留活动卡;
|
|
2361
2438
|
// 显式确认(按钮)或新回合隐式更替后解除;打开/切走/刷新不解除(C-030)----
|
|
@@ -2384,7 +2461,7 @@ assert.deepEqual(
|
|
|
2384
2461
|
"完成提醒计入徽标等待分子但不计阻塞(R-01-002/AC-06)",
|
|
2385
2462
|
);
|
|
2386
2463
|
assert.deepEqual(
|
|
2387
|
-
buildRecent(holdSnap, [], NOW,
|
|
2464
|
+
buildRecent(holdSnap, [], NOW, {}, [], acks(1000)).map((e) => e.id),
|
|
2388
2465
|
[],
|
|
2389
2466
|
"未确认完成提醒不入历史区(分区不变量)",
|
|
2390
2467
|
);
|
|
@@ -2399,7 +2476,7 @@ assert.deepEqual(
|
|
|
2399
2476
|
"确认后完成提醒解除、退出活动区",
|
|
2400
2477
|
);
|
|
2401
2478
|
assert.deepEqual(
|
|
2402
|
-
buildRecent(holdSnap, [], NOW,
|
|
2479
|
+
buildRecent(holdSnap, [], NOW, {}, [], ackedMap).map((e) => e.id),
|
|
2403
2480
|
["sB"],
|
|
2404
2481
|
"确认后会话进入历史区",
|
|
2405
2482
|
);
|
|
@@ -2457,7 +2534,7 @@ assert.deepEqual(
|
|
|
2457
2534
|
"错误提醒计入徽标等待分子但不计阻塞(R-01-002/AC-06)",
|
|
2458
2535
|
);
|
|
2459
2536
|
assert.deepEqual(
|
|
2460
|
-
buildRecent(holdSnap, [], NOW,
|
|
2537
|
+
buildRecent(holdSnap, [], NOW, {}, [], errAcks("error", "boom")).map((e) => e.id),
|
|
2461
2538
|
[],
|
|
2462
2539
|
"错误提醒中会话不入历史区(分区不变量,R-01-010/AC-06)",
|
|
2463
2540
|
);
|
|
@@ -2503,7 +2580,7 @@ assert.deepEqual(
|
|
|
2503
2580
|
["sB"],
|
|
2504
2581
|
"上一帧活动区 id 离开活动区且出现于历史区判定为迁移",
|
|
2505
2582
|
);
|
|
2506
|
-
assert.deepEqual(movedToRecentIds(new Set(["sB"]), [], []), [], "
|
|
2583
|
+
assert.deepEqual(movedToRecentIds(new Set(["sB"]), [], []), [], "彻底消失(归档/不再被会话服务列出)不判定为迁移");
|
|
2507
2584
|
assert.deepEqual(movedToRecentIds(new Set(), [{ id: "sB" }], []), [], "上一帧不在活动区不判定为迁移");
|
|
2508
2585
|
assert.deepEqual(movedToRecentIds(new Set(["sB"]), [{ id: "sB" }], []), [], "仍在活动区不判定为迁移");
|
|
2509
2586
|
|
|
@@ -2513,7 +2590,7 @@ assert.deepEqual(
|
|
|
2513
2590
|
["rB"],
|
|
2514
2591
|
"上一帧历史区 id 离开历史区且出现于活动区判定为反向迁移",
|
|
2515
2592
|
);
|
|
2516
|
-
assert.deepEqual(movedToActiveIds(new Set(["rB"]), [], []), [], "
|
|
2593
|
+
assert.deepEqual(movedToActiveIds(new Set(["rB"]), [], []), [], "彻底消失(归档/不再被会话服务列出)不判定为反向迁移");
|
|
2517
2594
|
assert.deepEqual(movedToActiveIds(new Set(), [{ id: "rB" }], []), [], "上一帧不在历史区不判定为反向迁移");
|
|
2518
2595
|
assert.deepEqual(movedToActiveIds(new Set(["rB"]), [], [{ id: "rB" }]), [], "仍在历史区不判定为反向迁移");
|
|
2519
2596
|
|
|
@@ -2544,14 +2621,13 @@ const recentArchived = buildRecent(
|
|
|
2544
2621
|
},
|
|
2545
2622
|
[],
|
|
2546
2623
|
NOW,
|
|
2547
|
-
undefined,
|
|
2548
2624
|
{},
|
|
2549
2625
|
["sGone"],
|
|
2550
2626
|
);
|
|
2551
2627
|
assert.deepEqual(
|
|
2552
2628
|
recentArchived.map((e) => e.id),
|
|
2553
2629
|
["sKeep"],
|
|
2554
|
-
"
|
|
2630
|
+
"归档会话即使仍被会话服务列出也不入最近历史",
|
|
2555
2631
|
);
|
|
2556
2632
|
assert.deepEqual(
|
|
2557
2633
|
buildRecent(
|
|
@@ -2565,7 +2641,6 @@ assert.deepEqual(
|
|
|
2565
2641
|
},
|
|
2566
2642
|
[],
|
|
2567
2643
|
NOW,
|
|
2568
|
-
undefined,
|
|
2569
2644
|
{},
|
|
2570
2645
|
new Set(["sGone"]),
|
|
2571
2646
|
).map((e) => e.id),
|
|
@@ -2703,8 +2778,8 @@ assert.ok(bundle.includes("notifyLayoutChange"), "布局变化通知 sibling ove
|
|
|
2703
2778
|
assert.ok(bundle.includes('window.dispatchEvent(new Event("resize"))'), "布局变化派发标准 resize 通知");
|
|
2704
2779
|
assert.ok(bundle.includes("pane !== renderedPane"), "新窗格实例必须重置渲染签名");
|
|
2705
2780
|
assert.ok(
|
|
2706
|
-
clientSource.includes("const sig = JSON.stringify([listState, cardSignature(visibleEntries), pulseSurface]);"),
|
|
2707
|
-
"列表 phase
|
|
2781
|
+
clientSource.includes("const sig = JSON.stringify([listState, cardSignature(visibleEntries), pulseSurface, recentTimeSignature]);"),
|
|
2782
|
+
"列表 phase 与历史时间文案必须参与结构化渲染签名,空列表不得冻结在加载/失败状态(T-087)",
|
|
2708
2783
|
);
|
|
2709
2784
|
// R-01-013/AC-02 回归:卡片标题必须随快照更新——单卡渲染异常不得冻结其余卡片
|
|
2710
2785
|
// (此前渲染签名先于卡片循环提交且无异常隔离,故障卡及其后全部卡片永久滞留旧标题,
|
|
@@ -2950,7 +3025,8 @@ assert.ok(!bundle.includes("serviceTimer"), "服务发现不得保留后台定
|
|
|
2950
3025
|
assert.ok(!bundle.includes("frameProbeTimer"), "宿主 frame 发现不得保留后台定时器");
|
|
2951
3026
|
assert.ok(bundle.includes("conversationObserver"), "流式 DOM 观察绑定到 conversation seat");
|
|
2952
3027
|
assert.ok(bundle.includes("centerObserver"), "宿主结构变化通过 center 直接子节点通知处理");
|
|
2953
|
-
assert.ok(bundle.includes("setInterval(() => queueSync(), CLOCK_MS)"), "
|
|
3028
|
+
assert.ok(bundle.includes("setInterval(() => queueSync(), CLOCK_MS)"), "运行卡时长继续使用独立的 1 秒时钟");
|
|
3029
|
+
assert.ok(bundle.includes("RECENT_TIME_REFRESH_MS") && bundle.includes("syncRecentTimeClock"), "历史卡相对时间使用独立的分钟级刷新时钟(R-01-013/AC-05)");
|
|
2954
3030
|
// R-01-014/AC-01
|
|
2955
3031
|
// R-01-014/AC-02
|
|
2956
3032
|
// R-01-014/AC-03
|
|
@@ -3144,6 +3220,7 @@ assert.ok(
|
|
|
3144
3220
|
assert.ok(bundle.includes("lastTurnEndFromEvents") && bundle.includes("lastTurnEndFromTimings"), "回合结束时刻提取进入 bundle(R-01-010/AC-08)");
|
|
3145
3221
|
assert.ok(bundle.includes("delegatingIds, turnEnds)"), "turnEnds 注入 buildRecent(R-01-010/AC-09)");
|
|
3146
3222
|
assert.ok(bundle.includes("fmtRecentTime(entry.activityAt)"), "最近卡渲染读取精化后的 activityAt(R-01-010/AC-08)");
|
|
3223
|
+
assert.ok(bundle.includes("fmtRelativeAge") && bundle.includes("最后活动 ·"), "最近卡同时显示绝对日期时间与相对活动时间(R-01-013/AC-05)");
|
|
3147
3224
|
|
|
3148
3225
|
assert.ok(
|
|
3149
3226
|
bundle.indexOf('[data-kind="recent"] {') < bundle.indexOf(".dap-card[data-opening]"),
|
|
@@ -3390,12 +3467,54 @@ assert.ok(
|
|
|
3390
3467
|
);
|
|
3391
3468
|
|
|
3392
3469
|
// R-01-004/AC-03
|
|
3393
|
-
//
|
|
3470
|
+
// 滚动条随滚动或鼠标指针进入窗格显示:thumb 默认透明、data-scrolling/data-pointer-inside 时显示;
|
|
3471
|
+
// Firefox 路径在 @supports 门内,native scrollbar 与右缘调宽手柄保持可分别命中。
|
|
3394
3472
|
assert.ok(bundle.includes(".dap-scroll::-webkit-scrollbar-thumb {\n background: transparent;"), "滚动条 thumb 默认透明(不滚动时不显示)");
|
|
3395
3473
|
assert.ok(bundle.includes(".dap-scroll[data-scrolling]::-webkit-scrollbar-thumb"), "滚动中经 data-scrolling 显示滚动条");
|
|
3474
|
+
assert.ok(bundle.includes("[data-dsh-activity-pane][data-pointer-inside] .dap-scroll::-webkit-scrollbar-thumb"), "鼠标进入窗格经 data-pointer-inside 显示滚动条");
|
|
3475
|
+
assert.ok(bundle.includes("[data-dsh-activity-pane][data-pointer-inside] .dap-scroll::-webkit-scrollbar-thumb:hover"), "鼠标悬停 native thumb 使用高 specificity hover 规则");
|
|
3476
|
+
assert.ok(bundle.includes("var(--dsh-scrollbar-thumb-hover"), "native thumb hover 读取 DSH 主题 hover token");
|
|
3477
|
+
assert.ok(bundle.includes("scrollbar-gutter: stable;"), "滚动区稳定预留 native scrollbar 槽位");
|
|
3478
|
+
assert.ok(bundle.includes("margin-right: var(--dsh-scrollbar-width, 8px);"), "滚动区右侧按 DSH scrollbar 宽度留出调宽边界");
|
|
3479
|
+
assert.ok(
|
|
3480
|
+
bundle.includes("[data-dsh-activity-pane] .dap-resize {\n position: absolute;\n top: 0; right: 0; bottom: 0;"),
|
|
3481
|
+
"调宽手柄保持在 pane 右缘",
|
|
3482
|
+
);
|
|
3483
|
+
assert.ok(!bundle.includes(".dap-resize:hover"), "调宽手柄悬停不绘制高亮区域");
|
|
3484
|
+
assert.ok(bundle.includes('pane.addEventListener("pointerenter", onPanePointerEnter)'), "鼠标进入窗格绑定滚动条显示");
|
|
3485
|
+
assert.ok(bundle.includes('pane.addEventListener("pointerleave", onPanePointerLeave)'), "鼠标离开窗格绑定滚动条隐藏");
|
|
3486
|
+
assert.ok(bundle.includes('pane.removeEventListener("pointerenter", onPanePointerEnter)') && bundle.includes('pane.removeEventListener("pointerleave", onPanePointerLeave)'), "unbind 清理窗格指针监听(R-02-003/AC-02)");
|
|
3396
3487
|
assert.ok(bundle.includes("@supports not selector(::-webkit-scrollbar)") && bundle.includes("scrollbar-color: transparent transparent"), "Firefox 路径以 @supports 门隔离(防 Chromium 丢弃伪元素规则)");
|
|
3488
|
+
assert.ok(bundle.includes("[data-dsh-activity-pane][data-pointer-inside] .dap-scroll {\n scrollbar-color:"), "Firefox 路径经 data-pointer-inside 显示滚动条");
|
|
3397
3489
|
assert.ok(bundle.includes('scroll?.addEventListener("scroll", onScroll, { passive: true })'), "滚动监听置位 data-scrolling");
|
|
3398
|
-
assert.ok(
|
|
3490
|
+
assert.ok(
|
|
3491
|
+
bundle.includes('scroll?.removeEventListener("scroll", onScroll);') && bundle.includes('if (scrollHideTimer !== null) clearTimeout(scrollHideTimer);'),
|
|
3492
|
+
"unbind 同步清理滚动监听与隐藏定时器(R-02-003/AC-02)",
|
|
3493
|
+
);
|
|
3494
|
+
assert.ok(
|
|
3495
|
+
bundle.includes('recentMore?.removeEventListener("click", onRecentMoreClick);') && !bundle.includes("recentObserver?.disconnect();"),
|
|
3496
|
+
"unbind 同步清理加载更多按钮监听,历史分页不再保留 observer(R-02-003/AC-02)",
|
|
3497
|
+
);
|
|
3498
|
+
|
|
3499
|
+
// R-01-019/AC-01~AC-04 历史候选与手动分页渲染契约
|
|
3500
|
+
assert.equal(RECENT_PAGE_SIZE, 10, "历史分页批次固定为 10 条(R-01-019/AC-01)");
|
|
3501
|
+
assert.ok(bundle.includes("const RECENT_PAGE_SIZE = 10;"), "生成 bundle 保留 10 条历史分页批次配置(R-01-019/AC-01)");
|
|
3502
|
+
assert.ok(bundle.includes('const recent = recentCandidates.slice(0, recentVisibleCount);'), "历史区只渲染当前可见候选前缀(R-01-019/AC-01)");
|
|
3503
|
+
assert.ok(bundle.includes('const nextCount = Math.min(recentVisibleCount + RECENT_PAGE_SIZE, recentTotal);'), "按钮激活后追加最多一个 10 条批次(R-01-019/AC-02)");
|
|
3504
|
+
assert.ok(
|
|
3505
|
+
bundle.includes('<button class="dap-recent-more" type="button" hidden>加载更多...</button>') &&
|
|
3506
|
+
bundle.includes('recentMore?.addEventListener("click", onRecentMoreClick);'),
|
|
3507
|
+
"历史区底部提供原生加载更多按钮并绑定显式激活(R-01-019/AC-02)",
|
|
3508
|
+
);
|
|
3509
|
+
assert.ok(
|
|
3510
|
+
!bundle.includes("new window.IntersectionObserver") && !bundle.includes('class="dap-recent-tail"') && !bundle.includes("maybeLoadMoreRecent"),
|
|
3511
|
+
"历史分页不再由 IntersectionObserver、尾部 sentinel 或滚动检查自动触发(R-01-019/AC-01、AC-02)",
|
|
3512
|
+
);
|
|
3513
|
+
assert.ok(
|
|
3514
|
+
bundle.includes("recentMore.hidden = !recentHasMore;") && bundle.includes("recentMore.disabled = recentAppendQueued;"),
|
|
3515
|
+
"仍有候选时显示按钮、追加期间禁用,候选耗尽后隐藏(R-01-019/AC-03)",
|
|
3516
|
+
);
|
|
3517
|
+
assert.ok(bundle.includes('recentHasMore = recent.length < recentTotal;'), "历史候选耗尽后停止追加(R-01-019/AC-03)");
|
|
3399
3518
|
|
|
3400
3519
|
// R-01-018 回到顶部悬浮图标按钮
|
|
3401
3520
|
// R-01-018/AC-01、R-01-018/AC-03:骨架按钮默认 hidden,滚动监听按 TOP_THRESHOLD 阈值揭隐/隐藏;
|