Files
MailUI4Agents/client/electron/test/components/Sidebar.test.tsx
JianFeeeee 4d6e944220 feat(webui): 打开即已读 + 回复即已读
用户:「现在邮件需要完全手动标记是否已读而不支持点进去自动已读或者回复自动已读」。

## 改法

- **打开即已读**:MailView 里加一个 effect —— 当前邮件是 unread 且**真正可见**时
  调 `markRead`。两个刻意的细节:
  1. 窄屏下 MailView 可能已经渲染但被列表覆盖层盖住(NarrowStack)⇒ 必须等到
     `narrowPane === 'detail'` 才标,否则"滑过去但没看"的邮件也会被标已读;
  2. 只对 `unread` 发请求(已读的再标一次是白跑,还会让接口日志一直响)。
- **回复即已读**:ReplyBar 发送**成功之后**才标(发送失败不该把"我处理过了"记下来)。
- 手动标记按钮保留(显式动作仍然有用)。

## 端到端验证(真浏览器 + 真库)

造一封未读 → 浏览器里展开会话分组、点开那封邮件 →

    POST /read → 200
    mail_reads 里出现 (reader=gui-lab)
    冗余列 mails.status: unread → read

即"打开即已读"确实生效,而且是记在**读者维度**上(不会像旧的邮件级已读那样
被别人一标就没了)。

## 一个过程记录

前两次验证都"没有发出 /read 请求",我一度以为代码没生效。其实是**点错了对象**:
会话分组默认折叠,`button:has-text(主题)` 匹配到的是**分组那颗**,点它只展开、
不选中邮件(所以不触发已读 —— 这恰恰是正确行为)。展开后再点具体邮件行才触发。
判据必须点"用户真正会点的那一层",这句话这次又应验了。

套件:vitest 15 文件 / 258 用例全绿。
2026-09-14 13:12:36 +08:00

63 lines
2.8 KiB
TypeScript
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.

import { render, fireEvent } from '@testing-library/react';
import { beforeEach, describe, expect, it } from 'vitest';
import Sidebar from '../../src/components/Sidebar';
import { useUIStore } from '../../src/stores/uiStore';
import { useAuthStore } from '../../src/stores/authStore';
import { useMailStore } from '../../src/stores/mailStore';
import { useContactStore } from '../../src/stores/contactStore';
/*
* 侧栏导航**点了要去对地方**。
*
* 这个判据是补写的:用户报「侧边导航栏完全不可用,点击无法翻页」——
* 根因是「通信」合并成一项后我写成 `setViewMode(target || commTab || modes[0])`
* 而日历/联系人没有 `target` ⇒ 点它们会跳到 `commTab`(收件箱)。
*
* 我当时的验证只看了 DOM 结构与样式(导航项数、徽标、圆角、玻璃),
* **一次都没点过**,所以这个 bug 一路到了线上。结构类判据必须配一条"点它"的判据。
*/
describe('侧栏导航点击目标', () => {
let view: ReturnType<typeof render>;
beforeEach(() => {
useUIStore.getState().reset();
useAuthStore.setState({ user: { username: 'gui-lab', role: 'admin' } as never });
useMailStore.setState({ inbox: [] });
useContactStore.setState({ contacts: [] });
view = render(<Sidebar />);
});
// 按**可见文字**找按钮(不用 getByTitletitle 属性在这个环境里查不到,
// 而按钮文字就是用户点的东西 —— 判据要贴住用户实际看到的那一层)
const click = (label: string) => {
const btn = [...document.querySelectorAll('button')].find(b =>
(b.textContent || '').trim() === label
);
if (!btn) throw new Error(`找不到导航项:${label}(现有:${[...document.querySelectorAll('button')].map(b => b.textContent?.trim()).join('/')}`);
fireEvent.click(btn);
return useUIStore.getState().viewMode;
};
it('点「日历」去日历(不是回通信)', () => {
expect(click('日历')).toBe('calendar');
});
it('点「联系」去联系人', () => {
expect(click('联系')).toBe('contacts');
});
it('点「通信」去通信上次用的子页签', () => {
// ★ 改 store 之后必须**重新渲染**再点:否则组件的点击闭包里还是旧的 commTab
// (我第一版没 rerender拿到 'inbox',差点误判成代码有问题)。
// 这里也说明了另一件事:内部页签渲染在 App 里而不是 Sidebar 里,
// 所以 Sidebar 的单测无法通过"点页签"来设置它。
useUIStore.getState().setViewMode('sent');
view.rerender(<Sidebar />);
expect(click('通信')).toBe('sent');
});
it('★ 反向对照:从日历点「通信」不会停在日历', () => {
useUIStore.getState().setViewMode('calendar');
expect(click('通信')).toBe('inbox');
});
});