Files
MailUI4Agents/client/electron/test/manual/calendar-swipe-verify.mjs
JianFeeeee 0a4b98144c feat(webui): 通信二级页签与列表头一体 + 沉浸式(PWA 全屏)+ 日历滑动验收脚本
用户三条(同一线索):「通信页面的二级页面与其他位置极其割裂」、
「不支持沉浸式网页」、「日历页面还不支持左右滑动手势」。

## ① 二级页签不再割裂

页签原先是**带 shadow 的白色胶囊**浮在面板上,看起来像硬贴上去的另一套控件。
改成**下划线页签**:与列表头同一内边距、同一条下边框,选中态用蓝色下划线 +
`-mb-px` 压住分隔线(否则会出现"两条线"的接缝)。

## ② 沉浸式

根因不是 viewport(`viewport-fit=cover` 早就有了,安全区也接了
`env(safe-area-inset-*)`),而是**没有 Web App Manifest**:手机上"添加到主屏幕"后
打开仍然是带地址栏的网页。现在加了 `manifest.webmanifest`(`display: standalone`)
+ iOS 的 `apple-mobile-web-app-capable` / `black-translucent`(状态栏内容叠在页面上)。

manifest 放在**根路径**而不是 /assets/ 下:它里面的 `start_url`/`scope` 是相对
manifest 自己的 URL 解析的,挂在 /assets/ 下就得写 "../"。静态只挂了 `/` 与 `/assets/*`,
所以显式加了一条路由(并从 embed 读,而不是读磁盘 —— 前端产物必须与应用同源同版本)。
图标由项目唯一图标源生成 192/512(尺寸与声明一致,我用 struct 读文件头核对过)。

**实测**:`/manifest.webmanifest` → HTTP 200 `application/manifest+json`。

## ③ 日历滑动:补上真正的验收脚本

`test/manual/calendar-swipe-verify.mjs` 四条,含**反向对照**(纵向拖动不得翻页)
与前置断言。写它时又踩了一次自己的坑:标题真实格式是「2026 年 9 月」(数字与"年月"
之间有空格),我第一版正则按无空格写 ⇒ 匹配不到 ⇒ 三个值全是 null,
**看起来像"滑动没生效",其实是探针瞎了**。所以脚本里第一条就是"标题读得到"。

实测:9 月 →左滑→ 10 月 →右滑→ 9 月,纵向拖动不动。

## 顺带

把我为验收造的测试数据**归档**(不是删除):10 个 `/tmp/scrollprobe-*` 独立会话 +
12 封"滚动验收/窄屏验收样例"邮件。
2026-09-14 11:07:03 +08:00

92 lines
3.7 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 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;