Files
ClockLab/test/clock-model.test.js
T
AskaEth 9c7081b21d feat: ClockLab —— 可调速桌面时钟(数字/表盘,支持倍率、暂停、倒转、快进)
一个用于演练/演示的桌面时钟工具:两个窗口 —— 控制窗口(普通)与时钟窗口(无边框、
可拖动、可拖边缘改大小,由控制窗口开与关)。

功能
- 两个窗口各自的「始终置顶」开关
- 显示样式:纯数字 / 圆形表盘,切换立即生效
- 实时设定时间(含「填入当前显示时间」)
- 倍率:支持正数、负数(倒转)、0(暂停)、小数
- 暂停 / 继续(继续时回到暂停前的倍率,按钮文案会显示是几倍)
- 快进五档:+5 秒 / +1 分钟 / +5 分钟 / +10 分钟 / +30 分钟

实现要点
- 走时是「基准时间 + 基准时刻 + 倍率」的锚点式模型(不累加、零漂移);
  倍率、暂停、倒转、快进因此是同一套机制的四个用法,互不打架
- 跳转动画做在「呈现时间」这一层:数字滚动与表盘指针扫动共用同一个中间值,
  所以永远同步,也不会跨 12 点倒着转一圈
- 无边框窗口的拖动(CSS app-region + 控件 no-drag)与拖边缘改大小
  (自己算几何 + IPC setBounds,因为 frame:false 下系统边缘命中不可靠)

测试 152 例:走时模型 / 改大小几何 / 呈现时间 / 两个渲染进程(DOM 桩)
2026-09-15 01:31:07 +08:00

168 lines
6.4 KiB
JavaScript
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.
'use strict';
// 纯逻辑自测(不需要 Electron、不需要显示器):
// node test/clock-model.test.js
// 走时模型是所有功能的地基,这里把它单独钉死,GUI 的问题就不会和算法问题混在一起。
const clockModel = require('../src/clock-model');
let passed = 0;
const failures = [];
function check(name, ok, detail) {
if (ok) {
passed += 1;
console.log(`PASS ${name}${detail ? ` [${detail}]` : ''}`);
} else {
failures.push(name);
console.log(`FAIL ${name}${detail ? ` [${detail}]` : ''}`);
}
}
const T0 = 1_700_000_000_000; // 固定基准时刻,测试不依赖真实时钟
const SECOND = 1000;
console.log('--- 正常走时 ---');
{
const c = clockModel.create(T0);
check('初始倍率为 1', c.rate === 1);
check('走 1 秒真实时间 = 1 秒显示时间', clockModel.displayMs(c, T0 + SECOND) === T0 + SECOND);
check('走 1 小时真实时间 = 1 小时显示时间', clockModel.displayMs(c, T0 + 3600 * SECOND) === T0 + 3600 * SECOND);
}
{
// 分 3600 次推进,验证「基准 + 经过时间 × 倍率」不会像逐帧累加那样漂移
const c = clockModel.create(T0);
let now = T0;
for (let i = 0; i < 3600; i += 1) {
now += SECOND;
clockModel.displayMs(c, now);
}
check('一小时逐秒推进后零漂移', clockModel.displayMs(c, now) === T0 + 3600 * SECOND);
}
console.log('--- 倍率 ---');
{
const c = clockModel.create(T0);
clockModel.setRate(c, T0, 2);
check('倍率 2:1 秒真实时间走 2 秒', clockModel.displayMs(c, T0 + SECOND) === T0 + 2 * SECOND);
}
{
const c = clockModel.create(T0);
clockModel.setRate(c, T0, 0.5);
check('倍率 0.5:半速', clockModel.displayMs(c, T0 + SECOND) === T0 + 500);
}
{
const c = clockModel.create(T0);
clockModel.setRate(c, T0, -1);
check('倍率 -1:走 1 秒真实时间倒退 1 秒', clockModel.displayMs(c, T0 + SECOND) === T0 - SECOND);
check('倍率 -1:再走 5 秒仍在继续倒退(不是卡住)',
clockModel.displayMs(c, T0 + 6 * SECOND) === T0 - 6 * SECOND);
}
{
const c = clockModel.create(T0);
clockModel.setRate(c, T0, -2.5);
check('倍率 -2.5:小数 + 负数同时成立', clockModel.displayMs(c, T0 + 4 * SECOND) === T0 - 10 * SECOND);
}
console.log('--- 切换倍率不跳变 ---');
{
const c = clockModel.create(T0);
const before = clockModel.displayMs(c, T0 + 3000);
clockModel.setRate(c, T0 + 3000, 7);
const after = clockModel.displayMs(c, T0 + 3000);
check('切倍率的瞬间显示时间连续(同一时刻前后相等)', before === after, `${before} → ${after}`);
check('切换后按新倍率走', clockModel.displayMs(c, T0 + 4000) === before + 7 * SECOND);
}
console.log('--- 暂停 / 继续 ---');
{
const c = clockModel.create(T0);
clockModel.setRate(c, T0, 4);
clockModel.setRate(c, T0 + SECOND, 0);
const frozenAt = clockModel.displayMs(c, T0 + SECOND);
check('倍率 0:显示时间冻结', clockModel.displayMs(c, T0 + 60 * SECOND) === frozenAt);
check('暂停时记住了暂停前的倍率 4', c.lastRate === 4);
clockModel.setRate(c, T0 + 60 * SECOND, c.lastRate);
check('继续:从暂停处按原倍率接着走',
clockModel.displayMs(c, T0 + 61 * SECOND) === frozenAt + 4 * SECOND);
}
{
const c = clockModel.create(T0);
clockModel.setRate(c, T0, 0);
check('倍率一直是 0 时 lastRate 保持默认 1', c.lastRate === 1);
}
console.log('--- 快进 / 快退 ---');
{
const c = clockModel.create(T0);
const before = clockModel.displayMs(c, T0 + 2000);
clockModel.shift(c, T0 + 2000, 5 * SECOND);
check('+5 秒:显示时间立刻加 5 秒', clockModel.displayMs(c, T0 + 2000) === before + 5 * SECOND);
check('快进后基准确实变了', c.baseMs === before + 5 * SECOND && c.baseRealMs === T0 + 2000);
check('快进后继续按倍率走', clockModel.displayMs(c, T0 + 3000) === before + 6 * SECOND);
}
{
const c = clockModel.create(T0);
clockModel.setRate(c, T0, -1);
const before = clockModel.displayMs(c, T0 + 10 * SECOND);
clockModel.shift(c, T0 + 10 * SECOND, 30 * 60 * SECOND);
check('负倍率下 +30 分钟:先固化显示时间再平移(不与倒转打架)',
clockModel.displayMs(c, T0 + 10 * SECOND) === before + 30 * 60 * SECOND);
check('平移后负倍率继续生效',
clockModel.displayMs(c, T0 + 11 * SECOND) === before + 30 * 60 * SECOND - SECOND);
}
{
const c = clockModel.create(T0);
clockModel.setRate(c, T0, 0);
const before = clockModel.displayMs(c, T0 + 1000);
clockModel.shift(c, T0 + 1000, 10 * 60 * SECOND);
check('暂停状态下快进仍然生效,且之后继续冻结',
clockModel.displayMs(c, T0 + 60 * SECOND) === before + 10 * 60 * SECOND);
}
{
// 五个快进按钮的档位
const deltas = [5 * SECOND, 60 * SECOND, 5 * 60 * SECOND, 10 * 60 * SECOND, 30 * 60 * SECOND];
const c = clockModel.create(T0);
const before = clockModel.displayMs(c, T0);
for (const d of deltas) clockModel.shift(c, T0, d);
const total = deltas.reduce((a, b) => a + b, 0);
check('五档快进累加正确(+5s/+1m/+5m/+10m/+30m)',
clockModel.displayMs(c, T0) === before + total, `总计 ${total / 1000} 秒`);
}
console.log('--- 设定时间 ---');
{
const c = clockModel.create(T0);
clockModel.setRate(c, T0, 60);
clockModel.setTimeOfDay(c, T0 + 5000, 8, 30, 15);
const d = new Date(clockModel.displayMs(c, T0 + 5000));
check('设定为 08:30:15 且毫秒归零',
d.getHours() === 8 && d.getMinutes() === 30 && d.getSeconds() === 15 && d.getMilliseconds() === 0,
`${d.getHours()}:${d.getMinutes()}:${d.getSeconds()}.${d.getMilliseconds()}`);
check('设定后高倍率继续生效', clockModel.displayMs(c, T0 + 6000) === d.getTime() + 60 * SECOND);
}
{
const c = clockModel.create(T0);
clockModel.setTimeOfDay(c, T0, 0, 0, 0);
const d = new Date(clockModel.displayMs(c, T0));
check('设定为 00:00:00(边界)', d.getHours() === 0 && d.getMinutes() === 0 && d.getSeconds() === 0);
}
{
const c = clockModel.create(T0);
clockModel.setTimeOfDay(c, T0, 23, 59, 59);
const d = new Date(clockModel.displayMs(c, T0 + 2000));
check('23:59:59 后跨零点进位', d.getHours() === 0 && d.getMinutes() === 0 && d.getSeconds() === 1,
`${d.getHours()}:${d.getMinutes()}:${d.getSeconds()}`);
}
console.log('');
if (failures.length === 0) {
console.log(`全部通过:${passed}/${passed}`);
process.exit(0);
} else {
console.log(`通过 ${passed},失败 ${failures.length}:${failures.join(';')}`);
process.exit(1);
}