Files
MailUI4Agents/client/electron/test/manual/calendar-swipe-verify.mjs
JianFeeeee d11e6df9a8 feat(webui): 日历翻页过渡动画(方向跟随手势/按钮)
用户:「日历滑动页面为什么没有切换动画?」。

## 做法

`shift()` 里记下方向(手势与"上一页/下一页"按钮**都走这一个函数** ⇒ 方向只有一个来源),
容器用 **key + 声明的动画类**:

    <div key={`${anchor.getTime()}-${scale}`} className={`flex-1 min-h-0 flex ${slideClass}`}>

## 为什么不是"命令式加类"

第一版用 `el.classList.add()`(与视图切换动画同一套写法),**类根本没进 DOM**。
原因:切月时 `loading` 会把网格整块换成加载态、再换回来,命令式加上的类会被
React 的重渲染与那次重挂载抹掉。key 变化 ⇒ 新元素自带动画类出现 ⇒ 必然重放。

## 顺带修掉一个守卫 bug

`firstRender` 守卫原先只在 `el` 存在时才消费。挂载时 `loading=true` ⇒ 容器还没渲染
⇒ 守卫一直留着 ⇒ **用户第一次真正翻页的动画被吞掉**(实测:点第一下不动、第二下才动)。
现在无条件消费。

## 判据

`test/manual/calendar-swipe-verify.mjs` 增加三条(与滑动手势同一条线索):
反向对照"刚进页面不跑动画"(animationName=none)、翻页时 `cal-slide-next` +
`cal-in-next` 且时长 >0、反向翻页是 `cal-slide-prev`(方向正确性)。
**实测**:静止 none → 点下一页 `cal-slide-next`/`cal-in-next` 0.2s → 上一页 `cal-slide-prev`/`cal-in-prev`。
2026-09-14 11:27:37 +08:00

130 lines
5.1 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-14 用户:「日历页面还不支持左右滑动手势」)。
*
* 判据必须一起看,否则"滑一下就翻页"也能算过:
* ① 左滑 → 下一段(月/周/日三种刻度都走同一个 shift()
* ② 右滑 → 回到上一段(可逆)
* ③ **反向对照**:纵向拖动不翻页 —— 移动端最容易犯的手势错误就是把滚动当翻页
*
* 探针踩过的坑标题真实格式是「2026 年 9 月」(**数字与"年月"之间有空格**
* 我第一版正则写成 /^(\d{4})年(\d{1,2})月/ 匹配不到 ⇒ 返回 null ⇒ 三个值全是 null
* 看起来"滑动没生效",其实是探针瞎了。所以这里先断言标题读得到。
*
* 用法AGENTMAIL_DIST=<dist> ADMIN_USER=.. ADMIN_PW=.. node test/manual/calendar-swipe-verify.mjs
*/
import { openApp } from './narrow-probe-helper.mjs';
const { page, ctx } = await openApp({ width: 390, height: 844 });
const results = [];
const record = (name, ok, note) => {
results.push({ name, ok });
console.log(` ${ok ? '通过' : '失败'} ${name}${note ? ' — ' + note : ''}`);
};
try {
await page.waitForSelector('.narrow-nav', { timeout: 25000 });
await page.click('.narrow-nav button:has-text("日历")');
await page.waitForTimeout(1500);
const title = () =>
page.evaluate(() => {
const rx = /\d{4}\s*年\s*\d{1,2}\s*月/;
const c = [...document.querySelectorAll('*')]
.filter(e => rx.test(e.textContent) && e.children.length <= 1)
.map(e => e.textContent.trim())
.filter(t => t.length < 40)
.sort((a, b) => a.length - b.length);
return c[0] ?? null;
});
const swipe = (x1, y1, x2, y2) =>
page.evaluate(
([x1, y1, x2, y2]) => {
const mk = (type, x, y) =>
new TouchEvent(type, {
bubbles: true,
cancelable: true,
touches:
type === 'touchend'
? []
: [new Touch({ identifier: 1, target: document.body, clientX: x, clientY: y })],
changedTouches: [
new Touch({ identifier: 1, target: document.body, clientX: x, clientY: y })
]
});
const el = document.elementFromPoint(x1, y1);
el.dispatchEvent(mk('touchstart', x1, y1));
el.dispatchEvent(mk('touchend', x2, y2));
},
[x1, y1, x2, y2]
);
const before = await title();
record('前置:日历标题读得到(否则本判据无意义)', !!before, `标题=${before}`);
await swipe(320, 420, 80, 420);
await page.waitForTimeout(900);
const afterLeft = await title();
record('① 左滑翻到下一段', !!afterLeft && afterLeft !== before, `${before}${afterLeft}`);
await swipe(80, 420, 320, 420);
await page.waitForTimeout(900);
const afterRight = await title();
record('② 右滑滑回上一段', afterRight === before, `${afterLeft}${afterRight}`);
await swipe(200, 300, 195, 620);
await page.waitForTimeout(900);
const afterVertical = await title();
record('③ 反向对照:纵向拖动不翻页', afterVertical === afterRight, `仍为 ${afterVertical}`);
// ⑤ 翻页过渡动画(用户后续追问:「日历滑动页面为什么没有切换动画?」)
const slide = () =>
page.evaluate(() => {
const el = document.querySelector('[data-slide]');
if (!el) return null;
return {
slide: el.dataset.slide,
anim: getComputedStyle(el).animationName,
dur: getComputedStyle(el).animationDuration
};
});
const rest = await slide();
record(
'⑤ 反向对照:刚进页面时不跑翻页动画',
!!rest && rest.anim === 'none',
`animationName=${rest?.anim}`
);
await page.click('button[aria-label="下一页"]');
await page.waitForTimeout(700);
await page.click('button[aria-label="下一页"]');
await page.waitForTimeout(80);
const during = await slide();
record(
'⑤ 翻页时有方向正确的过渡动画',
during?.slide === 'cal-slide-next' && during?.anim === 'cal-in-next' && parseFloat(during.dur) > 0,
`class=${during?.slide} 动画=${during?.anim} ${during?.dur}`
);
// 反方向:上一页应当是 cal-slide-prev方向只有一个来源 = shift()
await page.waitForTimeout(900);
await page.click('button[aria-label="上一页"]');
await page.waitForTimeout(80);
const backDuring = await slide();
record(
'⑤ 反向翻页的方向也是对的prev',
backDuring?.slide === 'cal-slide-prev' && backDuring?.anim === 'cal-in-prev',
`class=${backDuring?.slide}`
);
// ④ 回到今天不该被手势判断干扰(按钮仍在)
const todayBtn = await page.locator('button:has-text("今天")').count();
record('④ 手势没有吃掉「今天」按钮', todayBtn > 0, `找到 ${todayBtn}`);
} finally {
await page.close();
await ctx.close();
}
const failed = results.filter(r => !r.ok);
console.log(`\n 日历滑动:${results.length - failed.length}/${results.length} 通过`);
if (failed.length) console.log(' 失败:' + failed.map(f => f.name).join('; '));
process.exitCode = failed.length ? 1 : 0;