用户:「日历滑动页面为什么没有切换动画?」。
## 做法
`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`。
130 lines
5.1 KiB
JavaScript
130 lines
5.1 KiB
JavaScript
/**
|
||
* 日历左右滑动手势验收(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;
|