mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-10-05 12:34:35 +08:00
fix: 优化微信发送日志展示
This commit is contained in:
@@ -38,6 +38,7 @@ export class WechatActionLogService {
|
||||
category: 'wechat_send' as const,
|
||||
source: record.origin,
|
||||
purpose: record.purpose,
|
||||
...(record.triggerType ? { triggerType: record.triggerType } : {}),
|
||||
timestamp: record.finishedAt || record.startedAt || record.createdAt,
|
||||
recipientType: record.recipientType,
|
||||
recipientId: record.recipientId,
|
||||
@@ -46,7 +47,9 @@ export class WechatActionLogService {
|
||||
contentPreview: record.contentPreview,
|
||||
status: record.sendStatus,
|
||||
errorCode: record.errorCode,
|
||||
reason: record.decisionReason
|
||||
reason: record.decisionReason,
|
||||
...(record.executionId ? { executionId: record.executionId } : {}),
|
||||
...(record.idempotencyKey ? { idempotencyKey: record.idempotencyKey } : {})
|
||||
}))
|
||||
.sort((left, right) => Date.parse(right.timestamp) - Date.parse(left.timestamp))
|
||||
}
|
||||
|
||||
@@ -45,7 +45,6 @@ import { runtimePlatform, supportsPersonalWechatSend } from './utils/runtime-env
|
||||
import { useToast } from './components/ui'
|
||||
import { AppUpdatePrompt } from './features/app-update/AppUpdatePrompt'
|
||||
import { GroupExitMonitorWorkspace } from './features/group-exit-monitor/GroupExitMonitorWorkspace'
|
||||
import { LogsWorkspace } from './features/logs/LogsWorkspace'
|
||||
import { selectContactAvatarRefreshUsernames } from './utils/contact-avatar'
|
||||
import {
|
||||
buildContactSearchIndex,
|
||||
@@ -2053,8 +2052,6 @@ function App(): React.ReactElement {
|
||||
onOpenSendSettings={openWechatSendSettings}
|
||||
/>
|
||||
)
|
||||
case 'logs':
|
||||
return <LogsWorkspace />
|
||||
case 'agent-hub':
|
||||
return <AgentHubWorkspace />
|
||||
case 'api':
|
||||
|
||||
@@ -46,13 +46,6 @@ function NavIcon({ page }: NavIconProps): React.ReactElement {
|
||||
<path d="m16.5 9 3 3-3 3" />
|
||||
</svg>
|
||||
)
|
||||
case 'logs':
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||
<path d="M6 5.5h12v13H6z" />
|
||||
<path d="M9 9h6M9 12h6M9 15h4" />
|
||||
</svg>
|
||||
)
|
||||
case 'export':
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||
|
||||
@@ -3,7 +3,6 @@ export type AppPage =
|
||||
| 'search'
|
||||
| 'report'
|
||||
| 'exit-monitor'
|
||||
| 'logs'
|
||||
| 'agent-hub'
|
||||
| 'export'
|
||||
| 'api'
|
||||
@@ -19,7 +18,6 @@ export const PRIMARY_NAV_ITEMS: NavigationItem[] = [
|
||||
{ id: 'search', label: '问问微信' },
|
||||
{ id: 'report', label: '日报' },
|
||||
{ id: 'exit-monitor', label: '退群监控' },
|
||||
// { id: 'logs', label: '日志' },
|
||||
{ id: 'agent-hub', label: 'Agent' },
|
||||
{ id: 'export', label: '导出' },
|
||||
{ id: 'api', label: 'API' },
|
||||
|
||||
@@ -15,9 +15,21 @@ import {
|
||||
type StatusFilter = 'all' | WechatActionStatus
|
||||
type PurposeFilter = 'all' | 'member_left_notification' | 'scheduled_report' | 'other'
|
||||
|
||||
const sourceLabel = (source: ActionLogEntry['source']): string => {
|
||||
if (source === 'member_monitor') return '退群监控'
|
||||
if (source === 'scheduled_report') return '定时日报'
|
||||
if (source === 'user_tts') return '文字转语音'
|
||||
if (source === 'unknown') return '未知来源'
|
||||
return source || '未知来源'
|
||||
}
|
||||
|
||||
const triggerLabel = (triggerType: ActionLogEntry['triggerType']): string =>
|
||||
triggerType === 'user' ? '用户触发' : '自动化'
|
||||
|
||||
const purposeLabel = (purpose: WechatActionPurpose): string => {
|
||||
if (purpose === 'member_left_notification') return '退群通知'
|
||||
if (purpose === 'scheduled_report') return '定时日报'
|
||||
if (purpose === 'tts_voice') return '文字转语音'
|
||||
return purpose || '其它动作'
|
||||
}
|
||||
|
||||
@@ -27,15 +39,10 @@ const contentTypeLabel = (type: ActionLogEntry['contentType']): string => {
|
||||
return '文字'
|
||||
}
|
||||
|
||||
const statusDetails = (
|
||||
entry: ActionLogEntry
|
||||
): { label: string; className: string } => {
|
||||
const statusDetails = (entry: ActionLogEntry): { label: string; className: string } => {
|
||||
if (entry.status === 'sent') return { label: '已发送', className: 'text-success' }
|
||||
if (entry.status === 'blocked') return { label: '被阻止', className: 'text-warning' }
|
||||
if (
|
||||
entry.errorCode === 'SEND_CAPABILITY_UNAVAILABLE' ||
|
||||
entry.errorCode === 'SEND_NOT_READY'
|
||||
) {
|
||||
if (entry.errorCode === 'SEND_CAPABILITY_UNAVAILABLE' || entry.errorCode === 'SEND_NOT_READY') {
|
||||
return { label: '发送能力不可用', className: 'text-warning' }
|
||||
}
|
||||
return { label: '失败', className: 'text-destructive' }
|
||||
@@ -49,6 +56,9 @@ const matchesPurpose = (entry: ActionLogEntry, filter: PurposeFilter): boolean =
|
||||
return entry.purpose === filter
|
||||
}
|
||||
|
||||
const dateBoundary = (value: string, endOfDay: boolean): number =>
|
||||
new Date(`${value}T${endOfDay ? '23:59:59.999' : '00:00:00'}`).getTime()
|
||||
|
||||
export function LogsWorkspace(): React.ReactElement {
|
||||
const [entries, setEntries] = React.useState<ActionLogEntry[]>([])
|
||||
const [loading, setLoading] = React.useState(true)
|
||||
@@ -56,6 +66,8 @@ export function LogsWorkspace(): React.ReactElement {
|
||||
const [keyword, setKeyword] = React.useState('')
|
||||
const [status, setStatus] = React.useState<StatusFilter>('all')
|
||||
const [purpose, setPurpose] = React.useState<PurposeFilter>('all')
|
||||
const [startDate, setStartDate] = React.useState('')
|
||||
const [endDate, setEndDate] = React.useState('')
|
||||
|
||||
React.useEffect(() => {
|
||||
let disposed = false
|
||||
@@ -77,15 +89,28 @@ export function LogsWorkspace(): React.ReactElement {
|
||||
|
||||
const visibleEntries = React.useMemo(() => {
|
||||
const query = keyword.trim().toLocaleLowerCase()
|
||||
const startTime = startDate ? dateBoundary(startDate, false) : Number.NEGATIVE_INFINITY
|
||||
const endTime = endDate ? dateBoundary(endDate, true) : Number.POSITIVE_INFINITY
|
||||
return entries.filter((entry) => {
|
||||
if (status !== 'all' && entry.status !== status) return false
|
||||
if (!matchesPurpose(entry, purpose)) return false
|
||||
const timestamp = Date.parse(entry.timestamp)
|
||||
if (timestamp < startTime || timestamp > endTime) return false
|
||||
if (!query) return true
|
||||
return [entry.recipientName, entry.recipientId, entry.contentPreview]
|
||||
return [
|
||||
entry.id,
|
||||
entry.source,
|
||||
entry.purpose,
|
||||
entry.executionId,
|
||||
entry.idempotencyKey,
|
||||
entry.recipientName,
|
||||
entry.recipientId,
|
||||
entry.contentPreview
|
||||
]
|
||||
.filter(Boolean)
|
||||
.some((value) => value!.toLocaleLowerCase().includes(query))
|
||||
})
|
||||
}, [entries, keyword, purpose, status])
|
||||
}, [endDate, entries, keyword, purpose, startDate, status])
|
||||
|
||||
return (
|
||||
<div className="h-full min-w-0 overflow-y-auto bg-canvas">
|
||||
@@ -94,7 +119,7 @@ export function LogsWorkspace(): React.ReactElement {
|
||||
<h1 className="text-xl font-semibold text-foreground">日志</h1>
|
||||
</header>
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-[minmax(220px,1fr)_180px_180px]">
|
||||
<div className="grid gap-3 sm:grid-cols-[minmax(220px,1fr)_180px_180px] lg:grid-cols-[minmax(220px,1fr)_180px_180px_150px_150px]">
|
||||
<Input
|
||||
type="search"
|
||||
aria-label="搜索日志"
|
||||
@@ -124,6 +149,18 @@ export function LogsWorkspace(): React.ReactElement {
|
||||
<SelectItem value="blocked">被阻止</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input
|
||||
type="date"
|
||||
aria-label="开始日期"
|
||||
value={startDate}
|
||||
onChange={(event) => setStartDate(event.target.value)}
|
||||
/>
|
||||
<Input
|
||||
type="date"
|
||||
aria-label="结束日期"
|
||||
value={endDate}
|
||||
onChange={(event) => setEndDate(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
@@ -140,12 +177,22 @@ export function LogsWorkspace(): React.ReactElement {
|
||||
return (
|
||||
<article
|
||||
key={entry.id}
|
||||
className="grid gap-3 rounded-md border border-border-subtle bg-surface px-4 py-3 shadow-surface"
|
||||
className="grid min-w-0 gap-3 rounded-md border border-border-subtle bg-surface px-4 py-3 shadow-surface"
|
||||
>
|
||||
<div className="flex flex-wrap items-start justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<strong className="text-sm text-foreground">{purposeLabel(entry.purpose)}</strong>
|
||||
<p className="mt-1 break-words text-sm text-foreground">发送给 {recipient}</p>
|
||||
<strong className="text-sm text-foreground">
|
||||
{purposeLabel(entry.purpose)}
|
||||
</strong>
|
||||
<p className="mt-1 min-w-0 text-sm text-foreground">
|
||||
发送给{' '}
|
||||
<span
|
||||
className="inline-block max-w-[min(65vw,28rem)] truncate whitespace-nowrap align-bottom"
|
||||
title={recipient}
|
||||
>
|
||||
{recipient}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right text-xs">
|
||||
<strong className={result.className}>{result.label}</strong>
|
||||
@@ -154,17 +201,64 @@ export function LogsWorkspace(): React.ReactElement {
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-1 text-xs text-muted-foreground sm:grid-cols-[140px_1fr]">
|
||||
<span>
|
||||
{entry.recipientType === 'group' ? '群聊' : '联系人'} · {entry.recipientId}
|
||||
</span>
|
||||
<span className="break-words">
|
||||
<div className="grid min-w-0 gap-1 text-sm text-foreground">
|
||||
<p className="whitespace-normal break-words">
|
||||
{contentTypeLabel(entry.contentType)} · {entry.contentPreview || '无内容预览'}
|
||||
</span>
|
||||
</p>
|
||||
{entry.reason ? (
|
||||
<p className="whitespace-normal break-words text-xs text-muted-foreground">
|
||||
{entry.reason}
|
||||
</p>
|
||||
) : null}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{triggerLabel(entry.triggerType)} · {sourceLabel(entry.source)}
|
||||
</p>
|
||||
</div>
|
||||
{entry.reason ? (
|
||||
<p className="break-words text-xs text-muted-foreground">{entry.reason}</p>
|
||||
) : null}
|
||||
<details className="min-w-0 border-t border-border-subtle pt-2 text-xs text-muted-foreground">
|
||||
<summary className="cursor-pointer select-none">查看详情</summary>
|
||||
<dl className="mt-2 grid min-w-0 gap-1 sm:grid-cols-[140px_minmax(0,1fr)]">
|
||||
<dt>接收对象 ID</dt>
|
||||
<dd className="min-w-0 truncate whitespace-nowrap" title={entry.recipientId}>
|
||||
{entry.recipientId}
|
||||
</dd>
|
||||
<dt>操作 ID</dt>
|
||||
<dd className="min-w-0 truncate whitespace-nowrap" title={entry.id}>
|
||||
{entry.id}
|
||||
</dd>
|
||||
{entry.executionId ? (
|
||||
<>
|
||||
<dt>执行 ID</dt>
|
||||
<dd
|
||||
className="min-w-0 truncate whitespace-nowrap"
|
||||
title={entry.executionId}
|
||||
>
|
||||
{entry.executionId}
|
||||
</dd>
|
||||
</>
|
||||
) : null}
|
||||
{entry.idempotencyKey ? (
|
||||
<>
|
||||
<dt>幂等键</dt>
|
||||
<dd
|
||||
className="min-w-0 truncate whitespace-nowrap font-mono"
|
||||
title={entry.idempotencyKey}
|
||||
>
|
||||
{entry.idempotencyKey}
|
||||
</dd>
|
||||
</>
|
||||
) : null}
|
||||
<dt>来源</dt>
|
||||
<dd className="min-w-0 truncate whitespace-nowrap" title={entry.source}>
|
||||
{entry.source}
|
||||
</dd>
|
||||
<dt>用途</dt>
|
||||
<dd className="min-w-0 truncate whitespace-nowrap" title={entry.purpose}>
|
||||
{entry.purpose}
|
||||
</dd>
|
||||
<dt>触发方式</dt>
|
||||
<dd>{entry.triggerType || 'automation'}</dd>
|
||||
</dl>
|
||||
</details>
|
||||
</article>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { AboutPage } from './pages/AboutPage'
|
||||
import { VoiceRecognitionPage } from './pages/VoiceRecognitionPage'
|
||||
import { TextToSpeechPage } from './pages/TextToSpeechPage'
|
||||
import { PersonalWechatSendPage } from './pages/PersonalWechatSendPage'
|
||||
import { LogsWorkspace } from '../logs/LogsWorkspace'
|
||||
import type { Contact } from '../../../../shared/types'
|
||||
import type { AIRuntimeModelConfig } from '../../../../shared/ai-provider'
|
||||
|
||||
@@ -102,6 +103,8 @@ export function SettingsWorkspace({
|
||||
onOpenTextToSpeechSettings={onOpenTextToSpeechSettings}
|
||||
/>
|
||||
)
|
||||
case 'wechat-action-logs':
|
||||
return <LogsWorkspace />
|
||||
case 'text-to-speech':
|
||||
return <TextToSpeechPage onNotice={onNotice} />
|
||||
case 'personal-wechat-send':
|
||||
|
||||
@@ -18,6 +18,7 @@ export const SETTINGS_NAVIGATION: SettingsNavigationGroup[] = [
|
||||
label: '智能能力',
|
||||
items: [
|
||||
{ id: 'wechat-send', label: '微信发送' },
|
||||
{ id: 'wechat-action-logs', label: '发送日志' },
|
||||
{ id: 'voice-recognition', label: '语音转文字' },
|
||||
{ id: 'text-to-speech', label: '文字转语音' },
|
||||
{ id: 'ai-model', label: 'AI 模型' }
|
||||
|
||||
@@ -4,6 +4,7 @@ export type SettingsCategoryId =
|
||||
| 'image-key'
|
||||
| 'voice-recognition'
|
||||
| 'wechat-send'
|
||||
| 'wechat-action-logs'
|
||||
| 'text-to-speech'
|
||||
| 'personal-wechat-send'
|
||||
| 'ai-model'
|
||||
|
||||
@@ -4,7 +4,8 @@ import type {
|
||||
WechatActionOrigin,
|
||||
WechatActionPurpose,
|
||||
WechatActionRecipient,
|
||||
WechatActionStatus
|
||||
WechatActionStatus,
|
||||
WechatActionTriggerType
|
||||
} from './wechat-action'
|
||||
|
||||
export interface ActionLogEntry {
|
||||
@@ -12,6 +13,7 @@ export interface ActionLogEntry {
|
||||
category: 'wechat_send'
|
||||
source: WechatActionOrigin
|
||||
purpose: WechatActionPurpose
|
||||
triggerType?: WechatActionTriggerType
|
||||
timestamp: string
|
||||
recipientType: WechatActionRecipient['type']
|
||||
recipientId: string
|
||||
@@ -21,4 +23,6 @@ export interface ActionLogEntry {
|
||||
status: WechatActionStatus
|
||||
errorCode?: WechatActionErrorCode
|
||||
reason?: string
|
||||
executionId?: string
|
||||
idempotencyKey?: string
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import { render, screen, waitFor, within } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { LogsWorkspace } from '../../src/renderer/src/features/logs/LogsWorkspace'
|
||||
@@ -10,19 +10,23 @@ const entries: ActionLogEntry[] = [
|
||||
category: 'wechat_send',
|
||||
source: 'member_monitor',
|
||||
purpose: 'member_left_notification',
|
||||
triggerType: 'automation',
|
||||
timestamp: '2026-09-03T08:30:21.000Z',
|
||||
recipientType: 'group',
|
||||
recipientId: '123456@chatroom',
|
||||
recipientName: '测试 q1',
|
||||
contentType: 'text',
|
||||
contentPreview: 'Shinven 已退出群聊',
|
||||
status: 'sent'
|
||||
status: 'sent',
|
||||
executionId: 'execution-member-1',
|
||||
idempotencyKey: 'member_left_notification:member-log'
|
||||
},
|
||||
{
|
||||
id: 'report-log',
|
||||
category: 'wechat_send',
|
||||
source: 'scheduled_report',
|
||||
purpose: 'scheduled_report',
|
||||
triggerType: 'automation',
|
||||
timestamp: '2026-09-03T07:00:00.000Z',
|
||||
recipientType: 'group',
|
||||
recipientId: 'report@chatroom',
|
||||
@@ -31,7 +35,26 @@ const entries: ActionLogEntry[] = [
|
||||
contentPreview: 'daily-report.png',
|
||||
status: 'failed',
|
||||
errorCode: 'SEND_CAPABILITY_UNAVAILABLE',
|
||||
reason: '个人微信发送能力不可用'
|
||||
reason: '个人微信发送能力不可用',
|
||||
executionId: 'execution-report-1',
|
||||
idempotencyKey: 'scheduled_report:report-log'
|
||||
},
|
||||
{
|
||||
id: 'blocked-log',
|
||||
category: 'wechat_send',
|
||||
source: 'user_tts',
|
||||
purpose: 'tts_voice',
|
||||
triggerType: 'user',
|
||||
timestamp: '2026-09-03T06:00:00.000Z',
|
||||
recipientType: 'contact',
|
||||
recipientId: 'wxid_long_recipient_1234567890@chatroom.example',
|
||||
recipientName: '文件传输助手',
|
||||
contentType: 'voice',
|
||||
contentPreview: 'voice_xxx.silk 中文正文可以自然换行',
|
||||
status: 'blocked',
|
||||
executionId: 'execution-blocked-1234567890',
|
||||
idempotencyKey:
|
||||
'member_left_notification:49023470180@chatroom:2026-09-03T06:00:00.000Z:very-long-tail'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -49,6 +72,18 @@ describe('LogsWorkspace', () => {
|
||||
await waitFor(() => expect(screen.getByText(/Shinven 已退出群聊/)).toBeVisible())
|
||||
expect(screen.getByText(/daily-report.png/)).toBeVisible()
|
||||
expect(screen.getByText('发送能力不可用')).toBeVisible()
|
||||
expect(screen.getByText(/自动化 · 退群监控/)).toBeVisible()
|
||||
expect(screen.getByText('被阻止')).toBeVisible()
|
||||
expect(screen.queryByText('执行 ID:execution-member-1')).not.toBeInTheDocument()
|
||||
|
||||
const details = screen.getAllByText('查看详情')[0]
|
||||
await user.click(details)
|
||||
expect(screen.getByText('execution-member-1')).toBeVisible()
|
||||
expect(screen.getByText('member_left_notification:member-log')).toBeVisible()
|
||||
expect(screen.getByText('member_left_notification:member-log')).toHaveClass(
|
||||
'whitespace-nowrap',
|
||||
'font-mono'
|
||||
)
|
||||
|
||||
const search = screen.getByRole('searchbox', { name: '搜索日志' })
|
||||
await user.type(search, '123456')
|
||||
@@ -64,5 +99,52 @@ describe('LogsWorkspace', () => {
|
||||
await user.click(screen.getByRole('combobox', { name: '筛选日志类型' }))
|
||||
await user.click(await screen.findByRole('option', { name: '退群通知' }))
|
||||
expect(screen.getByText('没有匹配的日志')).toBeVisible()
|
||||
|
||||
await user.click(screen.getByRole('combobox', { name: '筛选日志类型' }))
|
||||
await user.click(await screen.findByRole('option', { name: '全部类型' }))
|
||||
await user.click(screen.getByRole('combobox', { name: '筛选日志状态' }))
|
||||
await user.click(await screen.findByRole('option', { name: '全部状态' }))
|
||||
const startDate = document.querySelector<HTMLInputElement>('input[aria-label="开始日期"]')
|
||||
const endDate = document.querySelector<HTMLInputElement>('input[aria-label="结束日期"]')
|
||||
expect(startDate).not.toBeNull()
|
||||
expect(endDate).not.toBeNull()
|
||||
await user.clear(startDate!)
|
||||
await user.type(startDate!, '2026-09-03')
|
||||
await user.clear(endDate!)
|
||||
await user.type(endDate!, '2026-09-03')
|
||||
expect(screen.getByText(/Shinven 已退出群聊/)).toBeVisible()
|
||||
expect(screen.getByText(/daily-report.png/)).toBeVisible()
|
||||
|
||||
await user.clear(startDate!)
|
||||
await user.type(startDate!, '2026-09-04')
|
||||
expect(screen.getByText('没有匹配的日志')).toBeVisible()
|
||||
})
|
||||
|
||||
it('keeps long identifiers on one line and exposes complete values in details', async () => {
|
||||
Object.defineProperty(window, 'api', {
|
||||
configurable: true,
|
||||
value: { listWechatActionLogs: vi.fn().mockResolvedValue(entries) } as unknown as typeof window.api
|
||||
})
|
||||
render(<LogsWorkspace />)
|
||||
|
||||
const blockedCard = (await screen.findByText('被阻止')).closest('article')
|
||||
expect(blockedCard).not.toBeNull()
|
||||
const card = within(blockedCard!)
|
||||
const recipient = card.getByTitle('wxid_long_recipient_1234567890@chatroom.example')
|
||||
expect(recipient).toHaveClass('truncate', 'whitespace-nowrap')
|
||||
const key = card.getByTitle(
|
||||
'member_left_notification:49023470180@chatroom:2026-09-03T06:00:00.000Z:very-long-tail'
|
||||
)
|
||||
expect(key).toHaveClass('truncate', 'whitespace-nowrap', 'font-mono')
|
||||
expect(card.getByText('语音 · voice_xxx.silk 中文正文可以自然换行')).toHaveClass(
|
||||
'whitespace-normal',
|
||||
'break-words'
|
||||
)
|
||||
|
||||
await userEvent.click(card.getByText('查看详情'))
|
||||
expect(card.getByText('wxid_long_recipient_1234567890@chatroom.example')).toBeVisible()
|
||||
expect(
|
||||
card.getByText('member_left_notification:49023470180@chatroom:2026-09-03T06:00:00.000Z:very-long-tail')
|
||||
).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -143,6 +143,26 @@ test('NAV-01 NAV-02 every top-level page is unique and switchable', async () =>
|
||||
}
|
||||
})
|
||||
|
||||
test('LOGS-01 shows Action Gateway audit details and filters by date', async () => {
|
||||
const fixture = await launchTestApp({ now: Date.parse('2026-09-12T10:00:00+08:00') })
|
||||
try {
|
||||
await fixture.page.getByRole('navigation', { name: '一级导航' }).getByRole('button', { name: '设置' }).click()
|
||||
await fixture.page.getByRole('button', { name: '发送日志' }).click()
|
||||
await expect(fixture.page.getByRole('heading', { name: '日志', exact: true })).toBeVisible()
|
||||
await expect(
|
||||
fixture.page.locator('article').filter({ hasText: '产品测试群' })
|
||||
).toBeVisible()
|
||||
await fixture.page.getByText('查看详情').first().click()
|
||||
await expect(fixture.page.getByText('fixture-execution', { exact: true })).toBeVisible()
|
||||
await expect(fixture.page.getByText(/自动化 · 定时日报/)).toBeVisible()
|
||||
|
||||
await fixture.page.getByLabel('开始日期').fill('2026-09-13')
|
||||
await expect(fixture.page.getByText('没有匹配的日志')).toBeVisible()
|
||||
} finally {
|
||||
await fixture.close()
|
||||
}
|
||||
})
|
||||
|
||||
test('NAV-03 exit monitor page shows a member departure event and stays within the viewport', async () => {
|
||||
const fixture = await launchTestApp()
|
||||
const pageErrors: Error[] = []
|
||||
|
||||
@@ -701,6 +701,24 @@ handle('db:getContactAvatars', (usernames) =>
|
||||
.map((contact) => [contact.m_nsUsrName, contact.avatar])
|
||||
)
|
||||
)
|
||||
handle('wechat-action-log:list', () => [
|
||||
{
|
||||
id: 'fixture-action-log',
|
||||
category: 'wechat_send',
|
||||
source: 'scheduled_report',
|
||||
purpose: 'scheduled_report',
|
||||
triggerType: 'automation',
|
||||
timestamp: new Date(fixtureNowMs - 60_000).toISOString(),
|
||||
recipientType: 'group',
|
||||
recipientId: 'group_regular@chatroom',
|
||||
recipientName: '产品测试群',
|
||||
contentType: 'image',
|
||||
contentPreview: 'fixture-report.png',
|
||||
status: 'sent',
|
||||
executionId: 'fixture-execution',
|
||||
idempotencyKey: 'scheduled_report:fixture-execution'
|
||||
}
|
||||
])
|
||||
handle('settings:getSelf', () => ({ ready: true, info: fixture.self }))
|
||||
handle('db:getCachedMessages', (md5) => fixture.messages[md5] || [])
|
||||
handle('db:getCachedMessagePage', (md5) => ({
|
||||
|
||||
Reference in New Issue
Block a user