feat: 完善定时日报

This commit is contained in:
Wxw-Gu
2026-08-27 19:03:07 +08:00
parent 949e6bc868
commit 7cf043e3c3
13 changed files with 1866 additions and 109 deletions
@@ -0,0 +1,111 @@
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ScheduledReportsWorkspace } from '../../src/renderer/src/components/reports/ScheduledReportsWorkspace'
import type { Contact } from '../../src/shared/types'
import type { PersonalWechatSendCapability } from '../../src/shared/personal-wechat'
const group: Contact = {
md5: 'group-md5',
m_nsUsrName: 'group@chatroom',
m_nsNickName: '技术交流',
type: 'group'
}
const capability = (
status: PersonalWechatSendCapability['status']
): PersonalWechatSendCapability => ({
supported: status !== 'unsupported',
ready: status === 'ready',
status,
capabilities: { text: status === 'ready', image: status === 'ready', voice: status === 'ready' },
senderStatus: {} as PersonalWechatSendCapability['senderStatus'],
message: ''
})
const task = {
id: 'task-1',
name: '技术交流 · 每日日报',
group: group.md5,
target: group.m_nsUsrName,
scheduleTime: '09:00',
reportRange: 'yesterday' as const,
enabled: true,
createdAt: '2026-08-26T00:00:00.000Z',
updatedAt: '2026-08-26T00:00:00.000Z',
nextRunAt: '2026-08-28T01:00:00.000Z'
}
describe('ScheduledReportsWorkspace', () => {
const onNotice = vi.fn()
const onOpenWechatSettings = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
Object.defineProperty(window, 'api', {
configurable: true,
value: {
getPersonalWechatSendCapability: vi.fn(async () => capability('ready')),
listScheduledReports: vi.fn(async () => [task]),
listScheduledReportExecutions: vi.fn(async () => []),
createScheduledReport: vi.fn(async () => ({ success: true, data: task })),
updateScheduledReport: vi.fn(async () => ({ success: true, data: task })),
deleteScheduledReport: vi.fn(async () => ({ success: true, data: { deletedId: task.id } })),
setScheduledReportEnabled: vi.fn(async () => ({ success: true, data: task })),
runScheduledReportNow: vi.fn(async () => ({ success: true, data: { status: 'success' } }))
}
})
})
it('loads capability and renders a scheduled report task', async () => {
render(
<ScheduledReportsWorkspace
contacts={[group]}
onNotice={onNotice}
onOpenWechatSettings={onOpenWechatSettings}
/>
)
expect(screen.getByText('正在检查微信发送能力…')).toBeInTheDocument()
await waitFor(() => expect(screen.getByText('✓ 微信发送能力已就绪')).toBeVisible())
expect(screen.getByText(task.name)).toBeVisible()
expect(screen.getByText('每天 09:00')).toBeVisible()
})
it('disables creation and offers settings when WeChat is unsupported', async () => {
vi.mocked(window.api.getPersonalWechatSendCapability).mockResolvedValue(
capability('unsupported')
)
render(
<ScheduledReportsWorkspace
contacts={[group]}
onNotice={onNotice}
onOpenWechatSettings={onOpenWechatSettings}
/>
)
await waitFor(() => expect(screen.getByText('微信消息发送目前仅支持 macOS')).toBeVisible())
expect(screen.getByRole('button', { name: /新建定时日报/ })).toBeDisabled()
})
it('opens the shared dialog and creates a task', async () => {
vi.mocked(window.api.listScheduledReports).mockResolvedValue([])
render(
<ScheduledReportsWorkspace
contacts={[group]}
onNotice={onNotice}
onOpenWechatSettings={onOpenWechatSettings}
/>
)
await waitFor(() => expect(screen.getByText('还没有定时日报')).toBeVisible())
fireEvent.click(screen.getAllByRole('button', { name: /新建定时日报/ })[0])
expect(screen.getByRole('dialog')).toBeVisible()
const dialog = screen.getByRole('dialog')
fireEvent.change(within(dialog).getByPlaceholderText('例如:技术交流 · 每日日报'), {
target: { value: '测试日报' }
})
fireEvent.click(within(dialog).getByRole('button', { name: '创建定时日报' }))
await waitFor(() =>
expect(window.api.createScheduledReport).toHaveBeenCalledWith(
expect.objectContaining({ name: '测试日报', group: group.md5, target: group.m_nsUsrName })
)
)
})
})
+64
View File
@@ -0,0 +1,64 @@
import { expect, test } from '@playwright/test'
import { launchTestApp } from './support/electron'
test('SCHEDULED-REPORT-UI-01 opens the scheduled report dialog without viewport overflow', async () => {
test.skip(process.platform !== 'darwin', 'The scheduled report send capability is macOS-only')
const fixture = await launchTestApp({ now: Date.parse('2026-08-27T08:30:00+08:00') })
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.setWindowContentSize({ width: 820, height: 600 })
await fixture.page.getByRole('button', { name: '日报' }).click()
await fixture.page.getByRole('tab', { name: '定时日报' }).click()
await expect(fixture.page.getByRole('heading', { name: '定时日报', exact: true })).toBeVisible()
await expect(fixture.page.getByText('✓ 微信发送能力已就绪')).toBeVisible()
const createButton = fixture.page.getByRole('button', { name: /新建定时日报/ }).first()
await expect(createButton).toBeEnabled()
await createButton.click()
const dialog = fixture.page.getByRole('dialog', { name: '新建定时日报' })
await expect(dialog).toBeVisible()
await expect(dialog.getByRole('button', { name: /产品测试群 .*微信群聊/ })).toBeVisible()
await expect(dialog.getByPlaceholder('搜索群聊')).toBeVisible()
await expect(dialog.getByRole('button', { name: '创建定时日报' })).toBeVisible()
const selects = dialog.getByRole('combobox')
await expect(selects).toHaveCount(2)
await selects.nth(0).click()
const templateOption = fixture.page.getByRole('option', { name: /Mobile 01.*微信信息流/ })
await expect(templateOption).toBeVisible()
await templateOption.click()
await expect(selects.nth(0)).toContainText('微信信息流')
await selects.nth(1).click()
const memberOption = fixture.page.getByRole('option', { name: '微信昵称' })
await expect(memberOption).toBeVisible()
await memberOption.click()
await expect(selects.nth(1)).toContainText('微信昵称')
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
const bounds = await dialog.boundingBox()
expect(bounds).not.toBeNull()
expect(bounds!.y).toBeGreaterThanOrEqual(0)
expect(bounds!.y + bounds!.height).toBeLessThanOrEqual(600)
await dialog.getByPlaceholder('例如:技术交流 · 每日日报').fill('E2E 定时日报')
await dialog.getByRole('radio', { name: '今天' }).click()
await dialog.getByRole('button', { name: '创建定时日报' }).click()
await expect(dialog).toHaveCount(0)
await expect(fixture.page.getByText('E2E 定时日报', { exact: true })).toBeVisible()
await expect(fixture.page.getByText('今天', { exact: true })).toBeVisible()
await fixture.page.getByRole('button', { name: '设置' }).click()
await fixture.page.getByRole('button', { name: '微信发送', exact: true }).click()
await expect(fixture.page.getByRole('heading', { name: '微信发送' })).toBeVisible()
await expect(fixture.page.getByRole('heading', { name: '发送能力' })).toBeVisible()
expect(pageErrors).toEqual([])
} finally {
await fixture.close()
}
})
+112
View File
@@ -234,6 +234,18 @@ const handle = (channel, fn) => {
ipcMain.handle(channel, async (event, ...args) => fn(...args))
}
const scheduledReportTasks = []
const scheduledReportExecutions = []
const scheduledReportNextRun = (scheduleTime) => {
const [hours, minutes] = String(scheduleTime || '09:00')
.split(':')
.map(Number)
const next = new Date(fixtureNowMs)
next.setHours(Number.isFinite(hours) ? hours : 9, Number.isFinite(minutes) ? minutes : 0, 0, 0)
if (next.getTime() <= fixtureNowMs) next.setDate(next.getDate() + 1)
return next.toISOString()
}
const startupCache = () => ({
self: fixture.self,
contacts,
@@ -299,6 +311,106 @@ handle('wechat-personal:getStatus', () => ({
canSendVoice: true,
message: '个人微信已绑定'
}))
handle('wechat-personal:getSendCapability', () => ({
supported: process.platform === 'darwin',
ready: process.platform === 'darwin',
status: process.platform === 'darwin' ? 'ready' : 'unsupported',
capabilities: {
text: process.platform === 'darwin',
image: process.platform === 'darwin',
voice: process.platform === 'darwin'
},
senderStatus: {
state: process.platform === 'darwin' ? 'online' : 'unsupported_platform',
platform: process.platform,
arch: process.arch,
sipDisabled: true,
wechatRunning: true,
boundWechatPid: 4668,
endpoint: '127.0.0.1:58080',
endpointReady: true,
runtimeReady: true,
attachReady: true,
baseAddressReady: true,
textHookInstalled: true,
textHookReady: true,
imageHookInstalled: true,
imageHookReady: true,
messageListenerReady: true,
canSend: process.platform === 'darwin',
canSendText: process.platform === 'darwin',
canSendImage: process.platform === 'darwin',
canSendVoice: process.platform === 'darwin',
message: '个人微信已绑定'
},
message:
process.platform === 'darwin' ? '个人微信已准备好发送日报' : '微信消息发送目前仅支持 macOS'
}))
handle('scheduled-report:list', () => [...scheduledReportTasks])
handle('scheduled-report:listExecutions', (taskId) =>
taskId
? scheduledReportExecutions.filter((execution) => execution.taskId === taskId)
: [...scheduledReportExecutions]
)
handle('scheduled-report:create', (request) => {
const now = new Date(fixtureNowMs).toISOString()
const task = {
id: `fixture-scheduled-report-${scheduledReportTasks.length + 1}`,
name: request.name,
group: request.group,
scheduleTime: request.scheduleTime,
reportRange: request.reportRange || 'yesterday',
messageTypes: request.messageTypes,
templateId: request.templateId,
memberNameMode: request.memberNameMode,
timeoutSeconds: request.timeoutSeconds,
target: request.target || request.group,
enabled: request.enabled !== false,
createdAt: now,
updatedAt: now,
nextRunAt: scheduledReportNextRun(request.scheduleTime)
}
scheduledReportTasks.push(task)
return { success: true, data: task }
})
handle('scheduled-report:update', (taskId, request) => {
const index = scheduledReportTasks.findIndex((task) => task.id === taskId)
if (index < 0) return { success: false, error: '任务不存在' }
const current = scheduledReportTasks[index]
const next = { ...current, ...request, updatedAt: new Date(fixtureNowMs).toISOString() }
if (request.scheduleTime) next.nextRunAt = scheduledReportNextRun(request.scheduleTime)
scheduledReportTasks[index] = next
return { success: true, data: next }
})
handle('scheduled-report:delete', (taskId) => {
const index = scheduledReportTasks.findIndex((task) => task.id === taskId)
if (index < 0) return { success: false, error: '任务不存在' }
scheduledReportTasks.splice(index, 1)
return { success: true, data: { deletedId: taskId } }
})
handle('scheduled-report:setEnabled', (taskId, enabled) => {
const task = scheduledReportTasks.find((item) => item.id === taskId)
if (!task) return { success: false, error: '任务不存在' }
task.enabled = Boolean(enabled)
task.updatedAt = new Date(fixtureNowMs).toISOString()
task.nextRunAt = task.enabled ? scheduledReportNextRun(task.scheduleTime) : task.nextRunAt
return { success: true, data: task }
})
handle('scheduled-report:runNow', (taskId) => {
const task = scheduledReportTasks.find((item) => item.id === taskId)
if (!task) return { success: false, error: '任务不存在' }
const execution = {
id: `fixture-execution-${scheduledReportExecutions.length + 1}`,
taskId,
startedAt: new Date(fixtureNowMs).toISOString(),
finishedAt: new Date(fixtureNowMs).toISOString(),
status: 'success',
message: '日报生成成功,微信发送成功'
}
scheduledReportExecutions.push(execution)
task.lastRunAt = execution.finishedAt
return { success: true, data: execution }
})
handle('wechat-share:getConfig', () => ({
success: true,
configured: true,