Files
MailUI4Agents/client/electron/test/animation-audit.test.mjs
JianFeeeee f14f2d6fa7 chore: 动画盘点判据接入套件 + 让其合规 + 对齐参照物重新登记(清工程收尾)
用户:「清理一下tmp和工程吧」。

- 新判据 test/animation-audit.test.mjs 接入 test/run-all.mjs:原先**写了却不会跑**
  (套件自带的那条闸门当场报「这些判据文件没接进套件」)。
- 该文件改用 test/lib/read.mjs 的具名入口(code/prose/bytes),不再裸用 readFileSync ——
  criteria-hygiene 抓到:读原文判代码会被解释性注释骗,今天已踩过两次。
- docs/ALIGN-REFS.json:CalendarView 的对齐登记按规矩**重新核对后再登记**
  (差异只有动画类 rise-in → pane-rise,骨架/布局/圆角来源未变;鸿蒙侧本无日历动效
  ⇒ 不产生新的对齐义务),不是抄一处新哈希。
2026-09-15 09:16:05 +08:00

116 lines
5.3 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.

/**
* 动画全量盘点 —— 把"整体动画"从"每次靠人眼看一遍"变成一条常驻判据。
*
* # 为什么需要它(2026-09-15 用户:「全面检查整体的动画」)
*
* 一次盘点查出两处**死动画**与一处**过宽的动画**,三种形态都极难靠肉眼发现:
* ① `@keyframes pane-in` 挂在 `html.view-switch .pane-enter` 上,而 **`.pane-enter`
* 没有任何组件在穿** —— 规则看着像"页面有入场动画",实际一次都不会播;
* ② `.animate-menu-in`(菜单入场)写好了、keyframes 也有,**同样没人穿**
* ⇒ 所有下拉/候选菜单其实都是"啪"地出现;
* ③ `html.view-switch .glass-control` 让**所有**控件档元素在每次切视图时一起动
* (几十个按钮/输入框同时淡入位移),是"闪"和卡顿的现成来源。
*
* 这三种都不是"动画不够好看",而是**接线断了**:类与使用者脱钩、作用域开得过大。
* 所以判据钉的是"接线",不是时长与曲线(那两样由人看着定)。
*
* # 判据
*
* 1. 每个 @keyframes 都必须有人穿 —— 从规则里取出穿它的类名,回源码里找;
* 找不到就是死动画(① 就是这么被抓住的)。
* 2. 弹层(.popup-surface 的菜单)必须带上菜单入场类(② 的接线)。
* 3. 不得再出现"整档控件一起动"的 view-switch 规则(③)。
* 4. 挂载即播那档(.rise-in)必须被 prefers-reduced-motion 显式关掉。
*/
import { readdirSync } from 'node:fs';
import { check, finish } from './lib/checks.mjs';
import { code } from './lib/read.mjs';
// 判"规则/代码里有没有这个东西"一律走剥注释版(code):解释性注释里会原样引用被禁的写法,
// 读原文会把它当成"还在用"(criteria-hygiene 就是这么抓到本文件第一版裸用 readFileSync 的)。
const css = code('src/index.css');
/** 组件源码全文:判"这个类有没有人穿"必须看代码,不是看我们的记忆 */
function componentsSrc() {
const out = [];
const walk = dir => {
for (const e of readdirSync(dir, { withFileTypes: true })) {
const p = `${dir}/${e.name}`;
if (e.isDirectory()) walk(p);
else if (/\.tsx?$/.test(e.name) && !/\.test\./.test(e.name)) out.push(code(p));
}
};
walk('src');
return out.join('\n');
}
const src = componentsSrc();
/** 取出所有 @keyframes 名 + 每条规则的 selector 文本 */
const keyframes = [...css.matchAll(/@keyframes\s+([a-zA-Z0-9_-]+)/g)].map(m => m[1]);
const rules = [...css.matchAll(/(^|\n)([^{}\n][^{}]*)\{([^{}]*)\}/g)].map(m => ({
sel: m[2].trim().replace(/\s+/g, ' '),
body: m[3]
}));
/**
* 一条 selector 在**最终**要不要靠某个类名才能命中?
* 只取"最后一个复合选择器"上的类(`html.view-switch .pane-enter` → `pane-enter`),
* 祖先里的类(html.view-switch、.app-shell)是状态开关,不算穿的人。
*/
function wearerClasses(sel) {
return sel
.split(',')
.map(part => part.trim().split(/\s+/).pop() || '')
.filter(last => last.startsWith('.'))
.map(last => last.split(/[\s.:[>]/)[0].replace(/^\./, ''))
.filter(Boolean);
}
const dead = [];
const unwrapped = [];
for (const k of keyframes) {
const users = rules.filter(r => new RegExp(`animation:\\s*${k}\\b`).test(r.body));
if (users.length === 0) {
dead.push(`${k}(没有任何规则用它)`);
continue;
}
// 这条 keyframes 的每一个使用者都必须"有人穿" —— 否则它还是不会播
const worn = users.some(r => {
const cls = wearerClasses(r.sel);
if (cls.length === 0) return true; // 元素选择器/通配:不算死
return cls.some(c => new RegExp(`["'\`\\s]${c}(?=["'\`\\s]|$)`).test(src) || src.includes(c));
});
if (!worn) unwrapped.push(`${k} ← ${users.map(u => u.sel).join(' | ')}`);
}
check(
'每个 @keyframes 都有人穿(没有死动画)',
dead.length === 0 && unwrapped.length === 0,
`只定义了没人穿的动画(看着有、永远不播):${[...dead, ...unwrapped].join(';')}`
);
check(
'弹层菜单带着菜单入场类',
/animate-menu-in/.test(rules.map(r => r.sel).join(',')) &&
/popup-surface[^`]*animate-menu-in|animate-menu-in[^`]*popup-surface/.test(src),
'菜单入场类存在但没人穿 —— 下拉/候选菜单是"啪"地出现(2026-09-15 盘点抓到的第二处死动画)'
);
// 只在**顶层规则**里查:reduced-motion 块里那句 `html.view-switch .glass-control` 是
// "把它关掉"的名单,不是"让它动"的规则 —— 第一版没区分,判据自己假红。
const topLevel = css.replace(/@media[^{]*\{(?:[^{}]|\{[^{}]*\})*\}/g, '');
check(
'没有"整档控件一起动"的切视图规则',
!/html\.view-switch\s+\.glass-control/.test(topLevel),
'view-switch 又把 .glass-control 整档带上了 —— 每次切视图几十个按钮/输入框一起动(闪与卡顿的现成来源)'
);
const reducedBlocks = css.match(/@media \(prefers-reduced-motion: reduce\)\s*\{[\s\S]*?\n\}/g) || [];
check(
'挂载即播那档被 reduced-motion 显式关掉',
reducedBlocks.some(b => /(^|\n)\s*\.rise-in\s*,/.test(b)),
'prefers-reduced-motion 覆盖不到 .rise-in —— 关掉动画的人照样会看到它'
);
finish('动画盘点');