mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-10-05 04:20:34 +08:00
453 lines
18 KiB
TypeScript
453 lines
18 KiB
TypeScript
import { render, screen, waitFor } from '@testing-library/react'
|
|
import userEvent from '@testing-library/user-event'
|
|
import * as React from 'react'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import {
|
|
GroupExitMonitorWorkspace,
|
|
type GroupExitMonitorOpenViewRequest
|
|
} from '../../src/renderer/src/features/group-exit-monitor/GroupExitMonitorWorkspace'
|
|
import type { GroupExitMonitorState } from '../../src/shared/group-exit-monitor'
|
|
import { formatGroupExitMonitorTime } from '../../src/shared/group-exit-monitor'
|
|
import type { Contact } from '../../src/shared/types'
|
|
import { TooltipProvider } from '../../src/renderer/src/components/ui'
|
|
|
|
const state: GroupExitMonitorState = {
|
|
events: [
|
|
{
|
|
id: 'event-1',
|
|
contactId: 'group-md5',
|
|
roomId: '研发群@chatroom',
|
|
groupName: '研发群',
|
|
memberWxid: 'wxid_alice',
|
|
memberName: '小艾',
|
|
wechatName: '小艾微信名',
|
|
groupRemark: '小艾群备注',
|
|
contactRemark: '小艾通讯录备注',
|
|
previousCount: 240,
|
|
currentCount: 239,
|
|
delta: -1,
|
|
message: '小艾退出了研发群',
|
|
detectedAt: 1_756_600_000_000
|
|
},
|
|
{
|
|
id: 'event-2',
|
|
contactId: 'group-design-md5',
|
|
roomId: '设计群@chatroom',
|
|
groupName: '设计群',
|
|
memberWxid: 'wxid_bob',
|
|
memberName: '小博',
|
|
wechatName: '小博微信名',
|
|
groupRemark: '',
|
|
previousCount: 80,
|
|
currentCount: 79,
|
|
delta: -1,
|
|
message: '小博退出了设计群',
|
|
detectedAt: 1_756_600_100_000
|
|
}
|
|
],
|
|
enabled: true,
|
|
running: true,
|
|
nativeMonitorActive: true,
|
|
monitoredGroupCount: 3,
|
|
monitorSelectionConfigured: true,
|
|
monitoredRoomIds: ['研发群@chatroom', '设计群@chatroom'],
|
|
lastCheckedAt: 1_756_600_000_000,
|
|
lastReadAt: 0,
|
|
unreadCount: 1
|
|
}
|
|
|
|
const groups: Contact[] = [
|
|
{
|
|
m_nsUsrName: '研发群@chatroom',
|
|
m_nsNickName: '研发群',
|
|
md5: 'group-md5',
|
|
type: 'group'
|
|
},
|
|
{
|
|
m_nsUsrName: '设计群@chatroom',
|
|
m_nsNickName: '设计群',
|
|
md5: 'group-design-md5',
|
|
type: 'group'
|
|
}
|
|
]
|
|
|
|
describe('GroupExitMonitorWorkspace', () => {
|
|
const renderWorkspace = (element: React.ReactElement): void => {
|
|
render(<TooltipProvider>{element}</TooltipProvider>)
|
|
}
|
|
|
|
beforeEach(() => {
|
|
window.api = {
|
|
getGroupExitMonitorState: vi.fn().mockResolvedValue(state),
|
|
onGroupExitMonitorState: vi.fn(() => () => undefined),
|
|
checkGroupExitMonitorNow: vi.fn().mockResolvedValue(state),
|
|
clearGroupExitMonitorEvents: vi.fn().mockResolvedValue({ ...state, events: [] })
|
|
} as typeof window.api
|
|
})
|
|
|
|
it('renders a semantic exit notification and member count delta', async () => {
|
|
renderWorkspace(<GroupExitMonitorWorkspace dbReady />)
|
|
|
|
await waitFor(() => expect(screen.getByText('小艾退出了研发群')).toBeVisible())
|
|
expect(screen.getByText(/240 人 → 239 人/)).toBeVisible()
|
|
expect(screen.getByText('小艾微信名')).toBeVisible()
|
|
expect(screen.getByText('小艾群备注')).toBeVisible()
|
|
expect(screen.getByText('小艾通讯录备注')).toBeVisible()
|
|
expect(screen.getByText('wxid_alice')).toBeVisible()
|
|
expect(screen.getByText('实时监听已启用')).toBeVisible()
|
|
})
|
|
|
|
it('persists the master switch and shows the paused state', async () => {
|
|
const user = userEvent.setup()
|
|
const pausedState = { ...state, enabled: false, running: false }
|
|
const setEnabled = vi.fn().mockResolvedValue(pausedState)
|
|
window.api = {
|
|
getGroupExitMonitorState: vi.fn().mockResolvedValue(state),
|
|
onGroupExitMonitorState: vi.fn(() => () => undefined),
|
|
setGroupExitMonitorEnabled: setEnabled,
|
|
checkGroupExitMonitorNow: vi.fn().mockResolvedValue(state),
|
|
clearGroupExitMonitorEvents: vi.fn().mockResolvedValue(state)
|
|
} as typeof window.api
|
|
|
|
renderWorkspace(<GroupExitMonitorWorkspace dbReady />)
|
|
const masterSwitch = await screen.findByRole('switch', { name: '退群监控总开关' })
|
|
expect(masterSwitch).toBeChecked()
|
|
await user.click(masterSwitch)
|
|
|
|
await waitFor(() => expect(setEnabled).toHaveBeenCalledWith(false))
|
|
expect(screen.getByText('监控已暂停')).toBeVisible()
|
|
expect(screen.getByRole('button', { name: '立即检查' })).toBeDisabled()
|
|
})
|
|
|
|
it('filters to groups that have exit events and clears the history', async () => {
|
|
const user = userEvent.setup()
|
|
renderWorkspace(<GroupExitMonitorWorkspace dbReady />)
|
|
|
|
await waitFor(() => expect(screen.getByText('小博退出了设计群')).toBeVisible())
|
|
await user.click(screen.getByRole('combobox', { name: '筛选退群群聊' }))
|
|
await user.click(await screen.findByRole('option', { name: /研发群/ }))
|
|
expect(screen.getByText('小艾退出了研发群')).toBeVisible()
|
|
expect(screen.queryByText('小博退出了设计群')).not.toBeInTheDocument()
|
|
|
|
await user.click(screen.getByRole('button', { name: '清空记录' }))
|
|
await waitFor(() => expect(screen.queryByText('小艾退出了研发群')).not.toBeInTheDocument())
|
|
expect(screen.getByText('暂无退群记录')).toBeVisible()
|
|
})
|
|
|
|
it('opens the management view, selects a group, and saves the monitoring scope', async () => {
|
|
const user = userEvent.setup()
|
|
const configuredState: GroupExitMonitorState = {
|
|
...state,
|
|
events: [],
|
|
monitorSelectionConfigured: true,
|
|
monitoredRoomIds: [],
|
|
monitoredGroupCount: 0,
|
|
unreadCount: 0
|
|
}
|
|
const savedState: GroupExitMonitorState = {
|
|
...configuredState,
|
|
monitoredRoomIds: ['研发群@chatroom'],
|
|
monitoredGroupCount: 1
|
|
}
|
|
const setGroups = vi.fn().mockResolvedValue(savedState)
|
|
window.api = {
|
|
getGroupExitMonitorState: vi.fn().mockResolvedValue(configuredState),
|
|
onGroupExitMonitorState: vi.fn(() => () => undefined),
|
|
setGroupExitMonitorGroups: setGroups,
|
|
checkGroupExitMonitorNow: vi.fn().mockResolvedValue(savedState),
|
|
clearGroupExitMonitorEvents: vi.fn().mockResolvedValue(savedState)
|
|
} as typeof window.api
|
|
|
|
renderWorkspace(<GroupExitMonitorWorkspace dbReady contacts={groups} />)
|
|
await waitFor(() => expect(screen.getByText('还没有设置监控群聊')).toBeVisible())
|
|
await user.click(screen.getByRole('button', { name: '选择群聊' }))
|
|
expect(screen.getByRole('heading', { name: '管理群聊' })).toBeVisible()
|
|
await user.click(screen.getByRole('checkbox', { name: '监控研发群' }))
|
|
await user.click(screen.getByRole('button', { name: '保存监控群聊' }))
|
|
|
|
await waitFor(() => expect(setGroups).toHaveBeenCalledWith(['研发群@chatroom']))
|
|
expect(screen.getByRole('heading', { name: '退群监控' })).toBeVisible()
|
|
expect(screen.getByText('已监控群聊')).toBeVisible()
|
|
})
|
|
|
|
it('filters group rows by selection and saves the monitoring scope', async () => {
|
|
const user = userEvent.setup()
|
|
const configuredState: GroupExitMonitorState = {
|
|
...state,
|
|
events: [],
|
|
monitorSelectionConfigured: true,
|
|
monitoredRoomIds: ['研发群@chatroom'],
|
|
monitoredGroupCount: 1,
|
|
unreadCount: 0
|
|
}
|
|
const savedState: GroupExitMonitorState = {
|
|
...configuredState,
|
|
}
|
|
const setGroups = vi.fn().mockResolvedValue(savedState)
|
|
window.api = {
|
|
getGroupExitMonitorState: vi.fn().mockResolvedValue(configuredState),
|
|
onGroupExitMonitorState: vi.fn(() => () => undefined),
|
|
getPersonalWechatSendCapability: vi.fn().mockResolvedValue({
|
|
supported: true,
|
|
ready: true,
|
|
status: 'ready',
|
|
capabilities: { text: true, image: true, voice: true },
|
|
senderStatus: {} as never,
|
|
message: '个人微信已准备好发送日报'
|
|
}),
|
|
setGroupExitMonitorGroups: setGroups,
|
|
checkGroupExitMonitorNow: vi.fn().mockResolvedValue(savedState),
|
|
clearGroupExitMonitorEvents: vi.fn().mockResolvedValue(savedState)
|
|
} as typeof window.api
|
|
|
|
renderWorkspace(<GroupExitMonitorWorkspace dbReady contacts={groups} />)
|
|
await user.click(await screen.findByRole('button', { name: '管理群聊' }))
|
|
await user.click(screen.getByRole('combobox', { name: '筛选群聊状态' }))
|
|
await user.click(await screen.findByRole('option', { name: '未选择' }))
|
|
expect(screen.getByRole('checkbox', { name: '监控设计群' })).toBeVisible()
|
|
expect(screen.queryByRole('checkbox', { name: '监控研发群' })).not.toBeInTheDocument()
|
|
|
|
await user.click(screen.getByRole('combobox', { name: '筛选群聊状态' }))
|
|
await user.click(await screen.findByRole('option', { name: '全部' }))
|
|
await user.click(screen.getByRole('button', { name: '保存监控群聊' }))
|
|
|
|
await waitFor(() => expect(setGroups).toHaveBeenCalledWith(['研发群@chatroom']))
|
|
})
|
|
|
|
it('selects every group even when a search term is active', async () => {
|
|
const user = userEvent.setup()
|
|
const configuredState: GroupExitMonitorState = {
|
|
...state,
|
|
events: [],
|
|
monitorSelectionConfigured: true,
|
|
monitoredRoomIds: [],
|
|
monitoredGroupCount: 0,
|
|
unreadCount: 0
|
|
}
|
|
const setGroups = vi.fn().mockResolvedValue({ ...configuredState, monitoredGroupCount: 2 })
|
|
window.api = {
|
|
getGroupExitMonitorState: vi.fn().mockResolvedValue(configuredState),
|
|
onGroupExitMonitorState: vi.fn(() => () => undefined),
|
|
setGroupExitMonitorGroups: setGroups,
|
|
checkGroupExitMonitorNow: vi.fn().mockResolvedValue(configuredState),
|
|
clearGroupExitMonitorEvents: vi.fn().mockResolvedValue(configuredState)
|
|
} as typeof window.api
|
|
|
|
renderWorkspace(<GroupExitMonitorWorkspace dbReady contacts={groups} />)
|
|
await user.click(await screen.findByRole('button', { name: '选择群聊' }))
|
|
await user.type(screen.getByRole('textbox', { name: '搜索群聊' }), '研发')
|
|
await user.click(screen.getByRole('button', { name: '全选' }))
|
|
await user.click(screen.getByRole('button', { name: '保存监控群聊' }))
|
|
|
|
await waitFor(() =>
|
|
expect(setGroups).toHaveBeenCalledWith(['研发群@chatroom', '设计群@chatroom'])
|
|
)
|
|
})
|
|
|
|
it('shows the send capability and explains an unavailable state on hover', async () => {
|
|
const user = userEvent.setup()
|
|
const capability = {
|
|
supported: true,
|
|
ready: false,
|
|
status: 'needs_binding' as const,
|
|
capabilities: { text: false, image: false, voice: false },
|
|
senderStatus: {} as never,
|
|
message: '请先绑定个人微信'
|
|
}
|
|
window.api = {
|
|
getGroupExitMonitorState: vi.fn().mockResolvedValue(state),
|
|
onGroupExitMonitorState: vi.fn(() => () => undefined),
|
|
getPersonalWechatSendCapability: vi.fn().mockResolvedValue(capability),
|
|
setGroupExitMonitorGroups: vi.fn().mockResolvedValue(state),
|
|
checkGroupExitMonitorNow: vi.fn().mockResolvedValue(state),
|
|
clearGroupExitMonitorEvents: vi.fn().mockResolvedValue(state)
|
|
} as typeof window.api
|
|
|
|
renderWorkspace(<GroupExitMonitorWorkspace dbReady contacts={groups} />)
|
|
await user.click(await screen.findByRole('button', { name: '管理群聊' }))
|
|
const status = screen.getByText('发送能力未就绪', { exact: true })
|
|
expect(status).toHaveClass('unready')
|
|
await user.hover(status)
|
|
await waitFor(() => expect(screen.getByRole('tooltip')).toHaveTextContent('请先绑定个人微信'))
|
|
})
|
|
|
|
/*
|
|
* 迁移后这里**不再有模板入口与「通知群聊」列**:
|
|
* 模板的唯一编辑处是「自动化 → 规则 → 退群通知」。
|
|
* 退群监控只负责监控范围与退群事实。
|
|
*/
|
|
it('管理群聊页不再提供通知模板与通知群聊配置', async () => {
|
|
const user = userEvent.setup()
|
|
window.api = {
|
|
getGroupExitMonitorState: vi.fn().mockResolvedValue(state),
|
|
onGroupExitMonitorState: vi.fn(() => () => undefined),
|
|
getPersonalWechatSendCapability: vi.fn().mockResolvedValue(null),
|
|
setGroupExitMonitorGroups: vi.fn().mockResolvedValue(state),
|
|
checkGroupExitMonitorNow: vi.fn().mockResolvedValue(state),
|
|
clearGroupExitMonitorEvents: vi.fn().mockResolvedValue(state)
|
|
} as typeof window.api
|
|
|
|
renderWorkspace(<GroupExitMonitorWorkspace dbReady contacts={groups} />)
|
|
await user.click(await screen.findByRole('button', { name: '管理群聊' }))
|
|
|
|
expect(screen.queryByRole('button', { name: '查看退群监测模板' })).toBeNull()
|
|
expect(screen.queryByRole('heading', { name: '退群监测模板' })).toBeNull()
|
|
expect(screen.queryByText('通知群聊')).toBeNull()
|
|
expect(screen.queryByLabelText('退群监测模板内容')).toBeNull()
|
|
// 监控选择本身仍然可用。
|
|
expect(screen.getByRole('checkbox', { name: '监控研发群' })).toBeInTheDocument()
|
|
})
|
|
|
|
it('复制退群信息用的模板来自自动化规则,而不是退群监控自己存的', async () => {
|
|
const user = userEvent.setup()
|
|
const template = [
|
|
'[退群监测]',
|
|
'用户: {user}',
|
|
'群备注: {groupRemark}',
|
|
'微信号: {wxid}',
|
|
'人数: {previousCount}->{currentCount}',
|
|
'退群时间: {time}'
|
|
].join('\n')
|
|
const copyText = vi.fn().mockResolvedValue({ success: true })
|
|
window.api = {
|
|
getGroupExitMonitorState: vi.fn().mockResolvedValue(state),
|
|
onGroupExitMonitorState: vi.fn(() => () => undefined),
|
|
copyText,
|
|
// 模板唯一来源:自动化里的退群通知规则。
|
|
listAutomationRules: vi.fn().mockResolvedValue([
|
|
{
|
|
id: 'builtin-leave-notification',
|
|
name: '退群通知',
|
|
enabled: true,
|
|
ruleType: 'leave_notification',
|
|
leaveNotification: { target: { type: 'file_transfer' }, template }
|
|
}
|
|
]),
|
|
checkGroupExitMonitorNow: vi.fn().mockResolvedValue(state),
|
|
clearGroupExitMonitorEvents: vi.fn().mockResolvedValue(state)
|
|
} as typeof window.api
|
|
|
|
renderWorkspace(<GroupExitMonitorWorkspace dbReady />)
|
|
await user.click(
|
|
await screen.findByRole('button', { name: '复制退群信息:小艾退出了研发群' })
|
|
)
|
|
|
|
await waitFor(() => expect(copyText).toHaveBeenCalledTimes(1))
|
|
expect(copyText).toHaveBeenCalledWith(
|
|
[
|
|
'[退群监测]',
|
|
'用户: 小艾微信名',
|
|
'群备注: 小艾群备注',
|
|
'微信号: wxid_alice',
|
|
'人数: 240->239',
|
|
`退群时间: ${formatGroupExitMonitorTime(state.events[0].detectedAt)}`
|
|
].join('\n')
|
|
)
|
|
expect(await screen.findByText('已复制')).toBeVisible()
|
|
})
|
|
|
|
it('reports a clipboard failure and keeps the event list intact', async () => {
|
|
const user = userEvent.setup()
|
|
const copyText = vi.fn().mockResolvedValue({ success: false, error: '剪贴板不可用' })
|
|
window.api = {
|
|
getGroupExitMonitorState: vi.fn().mockResolvedValue(state),
|
|
onGroupExitMonitorState: vi.fn(() => () => undefined),
|
|
copyText,
|
|
checkGroupExitMonitorNow: vi.fn().mockResolvedValue(state),
|
|
clearGroupExitMonitorEvents: vi.fn().mockResolvedValue(state)
|
|
} as typeof window.api
|
|
|
|
renderWorkspace(<GroupExitMonitorWorkspace dbReady />)
|
|
await user.click(
|
|
await screen.findByRole('button', { name: '复制退群信息:小艾退出了研发群' })
|
|
)
|
|
|
|
expect(await screen.findByText('剪贴板不可用')).toBeVisible()
|
|
expect(screen.getByText('小艾退出了研发群')).toBeVisible()
|
|
expect(screen.queryByText('已复制')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('leaves every monitoring checkbox off for a new installation', async () => {
|
|
const user = userEvent.setup()
|
|
const newInstallState: GroupExitMonitorState = {
|
|
...state,
|
|
events: [],
|
|
monitorSelectionConfigured: false,
|
|
monitoredRoomIds: [],
|
|
monitoredGroupCount: 0,
|
|
unreadCount: 0
|
|
}
|
|
window.api = {
|
|
getGroupExitMonitorState: vi.fn().mockResolvedValue(newInstallState),
|
|
onGroupExitMonitorState: vi.fn(() => () => undefined),
|
|
setGroupExitMonitorGroups: vi.fn().mockResolvedValue(newInstallState),
|
|
checkGroupExitMonitorNow: vi.fn().mockResolvedValue(newInstallState),
|
|
clearGroupExitMonitorEvents: vi.fn().mockResolvedValue(newInstallState)
|
|
} as typeof window.api
|
|
|
|
renderWorkspace(<GroupExitMonitorWorkspace dbReady contacts={groups} />)
|
|
await user.click(await screen.findByRole('button', { name: '管理群聊' }))
|
|
expect(screen.getByRole('checkbox', { name: '监控研发群' })).not.toBeChecked()
|
|
expect(screen.getByRole('checkbox', { name: '监控设计群' })).not.toBeChecked()
|
|
})
|
|
|
|
/*
|
|
* 深链请求必须是**一次性**的。
|
|
*
|
|
* 真实拓扑里请求挂在 App,而本组件随一级菜单切换被卸载/重挂。
|
|
* 修复前 effect 只 `setView` 不回报父层,请求永远不清 —— 于是重挂时
|
|
* 那条陈旧的 `{view:'manage'}` 被重放,用户看到的现象就是
|
|
* "点一级菜单退群监控,却跳到了管理群聊"。
|
|
*/
|
|
it('深链落到「管理群聊」,并立刻回报父层消费掉请求', async () => {
|
|
const onHandled = vi.fn()
|
|
renderWorkspace(
|
|
<GroupExitMonitorWorkspace
|
|
dbReady
|
|
openViewRequest={{ view: 'manage', requestId: 1 }}
|
|
onOpenViewRequestHandled={onHandled}
|
|
/>
|
|
)
|
|
|
|
expect(await screen.findByRole('heading', { name: '管理群聊' })).toBeVisible()
|
|
await waitFor(() => expect(onHandled).toHaveBeenCalledTimes(1))
|
|
})
|
|
|
|
it('请求被消费后重新挂载,回到「退群事件」而不是「管理群聊」', async () => {
|
|
function Harness(): React.ReactElement {
|
|
const [request, setRequest] = React.useState<GroupExitMonitorOpenViewRequest | null>({
|
|
view: 'manage',
|
|
requestId: 1
|
|
})
|
|
const [mounted, setMounted] = React.useState(true)
|
|
return (
|
|
<>
|
|
<button type="button" onClick={() => setMounted((prev) => !prev)}>
|
|
切换页面
|
|
</button>
|
|
{mounted ? (
|
|
<GroupExitMonitorWorkspace
|
|
dbReady
|
|
openViewRequest={request}
|
|
onOpenViewRequestHandled={() => setRequest(null)}
|
|
/>
|
|
) : null}
|
|
</>
|
|
)
|
|
}
|
|
|
|
const user = userEvent.setup()
|
|
renderWorkspace(<Harness />)
|
|
|
|
// 第一次:深链确实生效。
|
|
expect(await screen.findByRole('heading', { name: '管理群聊' })).toBeVisible()
|
|
|
|
// 离开再回来(等价于点别的菜单再点「退群监控」)。
|
|
await user.click(screen.getByRole('button', { name: '切换页面' }))
|
|
await user.click(screen.getByRole('button', { name: '切换页面' }))
|
|
|
|
expect(await screen.findByText('小艾退出了研发群')).toBeVisible()
|
|
expect(screen.queryByRole('heading', { name: '管理群聊' })).toBeNull()
|
|
})
|
|
})
|