chore: 打包配置 测试用例

This commit is contained in:
电摇小子
2026-09-29 13:06:00 +07:00
parent 0abbda1160
commit 5276bce060
18 changed files with 621 additions and 160 deletions
+41 -10
View File
@@ -197,15 +197,6 @@ test('NAV-04 exit monitor management can save an empty scope and show the setup
await expect(fixture.page.getByRole('heading', { name: '管理群聊', exact: true })).toBeVisible()
await expect(fixture.page.getByText('发送能力已就绪', { exact: true })).toBeVisible()
await expect(fixture.page.getByRole('checkbox', { name: '监控产品测试群' })).toBeChecked()
await fixture.page.getByRole('button', { name: '查看退群监测模板' }).click()
await expect(fixture.page.getByLabel('退群监测模板内容')).toHaveValue(/用户: \{user\}/)
await expect(fixture.page.getByLabel('退群监测模板内容')).toHaveValue(/群备注: \{groupRemark\}/)
const customTemplate = '[退群监测]\n用户: {user}\n群备注: {groupRemark}'
await fixture.page.getByLabel('退群监测模板内容').fill(customTemplate)
await fixture.page.getByRole('button', { name: '保存模板' }).click()
await fixture.page.getByRole('button', { name: '查看退群监测模板' }).click()
await expect(fixture.page.getByLabel('退群监测模板内容')).toHaveValue(customTemplate)
await fixture.page.keyboard.press('Escape')
await fixture.page.getByRole('checkbox', { name: '监控产品测试群' }).click()
await fixture.page.getByRole('checkbox', { name: '监控折叠群聊样本' }).click()
await fixture.page.getByRole('button', { name: '保存监控群聊' }).click()
@@ -216,6 +207,46 @@ test('NAV-04 exit monitor management can save an empty scope and show the setup
}
})
/**
* 退群通知的**内容模板**已随「退群通知」一起迁进自动化,管理群聊页只负责
* 「监测哪些群」。这条用例锁住新家:模板可编辑、可保存,且真的落库。
*/
test('NAV-05 leave notification template is editable in the automation workspace', async () => {
const fixture = await launchTestApp()
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.page.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.getByRole('heading', { name: '退群通知' })).toBeVisible()
const template = fixture.page.getByLabel('退群通知模板内容')
await expect(template).toHaveValue(/用户: \{user\}/)
await expect(template).toHaveValue(/群备注: \{groupRemark\}/)
const customTemplate = '[退群监测]\n用户: {user}\n群备注: {groupRemark}'
await template.fill(customTemplate)
await fixture.page.getByRole('button', { name: '保存' }).click()
/*
* `exact: true` 不能省:Radix 额外渲染一个 `role="status"` 的无障碍播报节点,
* 文本是 `Notification 已保存「退群通知」`,且由 `useNextFrame` **下一帧才填** ——
* 裸 regex 会命中它造成 strict mode violation,且是否命中取决于帧时序。
*/
await expect(fixture.page.getByText('已保存「退群通知」', { exact: true })).toBeVisible()
await fixture.page.reload()
await fixture.page.getByRole('button', { name: '退群监控' }).click()
await fixture.page.getByRole('button', { name: '配置退群通知自动化' }).click()
await expect(fixture.page.getByLabel('退群通知模板内容')).toHaveValue(customTemplate)
expect(pageErrors).toEqual([])
} finally {
await fixture.close()
}
})
test('CHAT-01 archive More menu is keyboard-safe and keeps the page usable', async () => {
const fixture = await launchTestApp()
const pageErrors: Error[] = []
@@ -773,7 +804,7 @@ test('AGENT-01 Agent Hub controls stay usable in the default offline layout', as
await fixture.page.keyboard.press('Escape')
await expect(logSource).toBeFocused()
await expect(fixture.page.getByRole('button', { name: '复制日志' })).toBeDisabled()
await fixture.page.getByRole('button', { name: '清空' }).click()
await fixture.page.getByRole('button', { name: '清空', exact: true }).click()
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
+146 -99
View File
@@ -1,80 +1,146 @@
import { expect, test } from '@playwright/test'
import { expect, test, type Page } from '@playwright/test'
import { launchTestApp } from './support/electron'
const debugNotificationEnabled = ['1', 'true', 'yes', 'on'].includes(
String(process.env.VITE_SCHEDULED_REPORT_DEBUG || '')
.trim()
.toLowerCase()
)
/**
* 定时日报的 E2E。
*
* 「定时日报」已从「日报 → 定时日报」独立页迁到「自动化 → 规则 → 定时日报」,
* 本文件走的是**迁移后**的真实路径:
*
* 日报(提示条)→「去自动化配置 →」→ 自动化 · 定时日报(规则列表)→ 新建 → 编辑器
*
* 断言一律打在真实模型渲染出的**默认值与真实选项**上(范围 / 模板 / 成员名称 /
* 消息类型 / 发送目标),而不是「页面上有个按钮」这种空转检查。
*/
test('SCHEDULED-REPORT-UI-01 opens the scheduled report dialog without viewport overflow', async () => {
/** fixture 约定的固定时刻:2026-08-27 08:30 (+08:00)。 */
const FIXTURE_NOW = Date.parse('2026-08-27T08:30:00+08:00')
/** 从时间线进入「自动化 → 定时日报」的规则列表。 */
async function openScheduledReportRuleList(page: Page): Promise<void> {
await page.getByRole('button', { name: '日报' }).click()
await page.getByRole('button', { name: '去自动化配置 →' }).click()
await expect(page.getByRole('radiogroup', { name: '切换规则类型' })).toBeVisible()
}
/** 列表 → 新建 → 编辑器。 */
async function openScheduledReportEditor(page: Page): Promise<void> {
await openScheduledReportRuleList(page)
await page.getByRole('button', { name: '+ 新建定时日报' }).first().click()
await expect(page.getByRole('heading', { name: '新建定时日报' })).toBeVisible()
}
test('SCHEDULED-REPORT-UI-01 creates a scheduled report rule from the automation workspace without viewport overflow', async () => {
test.skip(
process.platform !== 'darwin' && process.platform !== 'win32',
'The scheduled report send capability requires macOS or Windows'
)
const fixture = await launchTestApp({ now: Date.parse('2026-08-27T08:30:00+08:00') })
const fixture = await launchTestApp({ now: FIXTURE_NOW })
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
const page = fixture.page
try {
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()
await openScheduledReportRuleList(page)
const createButton = fixture.page.getByRole('button', { name: /新建定时日报/ }).first()
await expect(createButton).toBeEnabled()
await createButton.click()
// 深链把「定时日报」这个类型选中,并且先给列表而不是直接甩一个空表单。
await expect(page.getByRole('radio', { name: '定时日报' })).toBeChecked()
await expect(page.getByText('还没有定时日报')).toBeVisible()
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()
await page.getByRole('button', { name: '+ 新建定时日报' }).first().click()
await expect(page.getByRole('heading', { name: '新建定时日报' })).toBeVisible()
const customRange = dialog.locator('[aria-disabled="true"]')
await expect(customRange).toHaveCount(1)
const customLabel = customRange.getByText('自定义', { exact: true })
const customHint = customRange.getByText('即将支持', { exact: true })
await expect(customLabel).toHaveCSS('white-space', 'nowrap')
await expect(customHint).toHaveCSS('white-space', 'nowrap')
expect((await customLabel.boundingBox())?.height).toBeLessThanOrEqual(20)
expect((await customHint.boundingBox())?.height).toBeLessThanOrEqual(20)
// 编辑器是内嵌面板(不是弹窗),三段结构齐全。
for (const heading of ['1 · 什么时候触发', '2 · 生成什么日报', '3 · 生成后发送到哪里']) {
await expect(page.getByRole('heading', { name: heading })).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('微信信息流')
// 触发段:默认 18:30,且「预计下次执行」按本地时区算出当日槽位。
await expect(page.getByRole('textbox', { name: '执行时间小时' })).toHaveValue('18')
await expect(page.getByRole('textbox', { name: '执行时间分钟' })).toHaveValue('30')
await expect(page.getByTestId('scheduled-next-run')).toHaveText('今日 18:30')
await selects.nth(1).click()
const memberOption = fixture.page.getByRole('option', { name: '微信昵称' })
await expect(memberOption).toBeVisible()
await memberOption.click({ force: true })
await expect(selects.nth(1)).toContainText('微信昵称', { timeout: 8000 })
// 还没填必填项就必须**说不出原因地**拦住保存。
const saveButton = page.getByRole('button', { name: '保存', exact: true })
await expect(saveButton).toBeDisabled()
await expect(page.getByTestId('scheduled-save-blocker')).toHaveText('请填写任务名称')
/*
* 「自定义」区间在旧页面是 aria-disabled 的占位,新编辑器直接把范围收成一个下拉 ——
* 所以这里不再断言那个占位,改成断言下拉本身接的是真实取值。
*/
await expect(page.getByRole('combobox', { name: '日报范围' })).toHaveText('今日')
// 来源群候选来自真实群列表,且搜索框真的过滤。
await expect(page.getByRole('radio', { name: '产品测试群' })).toBeVisible()
await expect(page.getByRole('radio', { name: '折叠群聊样本' })).toBeVisible()
await page.getByRole('textbox', { name: '搜索群聊' }).fill('折叠')
await expect(page.getByRole('radio', { name: '产品测试群' })).toHaveCount(0)
await expect(page.getByRole('radio', { name: '折叠群聊样本' })).toBeVisible()
await page.getByRole('textbox', { name: '搜索群聊' }).fill('')
await expect(page.getByRole('radio', { name: '产品测试群' })).toBeVisible()
// 模板下拉:默认经典日报,能切到社区模板。
const templateSelect = page.getByRole('combobox', { name: '日报模板' })
await expect(templateSelect).toHaveText('经典日报')
await templateSelect.click()
await page.getByRole('option', { name: 'Mobile 01 · 微信信息流' }).click()
await expect(templateSelect).toHaveText('Mobile 01 · 微信信息流')
// 成员名称下拉:默认群昵称,能切到微信昵称。
const memberSelect = page.getByRole('combobox', { name: '成员名称' })
await expect(memberSelect).toHaveText('群昵称')
await memberSelect.click()
await page.getByRole('option', { name: '微信昵称' }).click()
await expect(memberSelect).toHaveText('微信昵称')
await expect(page.getByRole('textbox', { name: '日报生成超时' })).toHaveValue('300')
// 发送目标四选一,默认「文件传输助手」。
await expect(page.getByRole('radio', { name: '文件传输助手' })).toBeChecked()
// 整页不允许出现横向滚动条(布局回归的硬门槛)。
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
await 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(
await fixture.page.evaluate(() => window.innerHeight)
)
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 page.getByRole('textbox', { name: '定时日报任务名称' }).fill('E2E 定时日报')
await expect(page.getByTestId('scheduled-save-blocker')).toHaveText('请选择日报来源群')
await expect(saveButton).toBeDisabled()
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: '发送能力', exact: true })).toBeVisible()
await page.getByRole('radio', { name: '产品测试群' }).click()
await expect(page.getByTestId('scheduled-save-blocker')).toHaveCount(0)
await expect(saveButton).toBeEnabled()
await saveButton.click()
// toast 必须 `exact`:Radix 的无障碍播报节点文案是 `Notification 已创建「…」`,子串会命中 2 个。
await expect(page.getByText('已创建「E2E 定时日报」', { exact: true })).toBeVisible()
// 回到列表,卡片渲染的是**保存回来的规则**,不是草稿。
await expect(page.getByRole('heading', { name: 'E2E 定时日报' })).toBeVisible()
await expect(page.getByText('每天 18:30')).toBeVisible()
await expect(page.getByRole('button', { name: '+ 新建定时日报' }).first()).toBeVisible()
// 发送能力状态仍是设置页里的同一份事实。
await page.getByRole('button', { name: '设置' }).click()
await page.getByRole('button', { name: '微信发送', exact: true }).click()
await expect(page.getByRole('heading', { name: '微信发送' })).toBeVisible()
/*
* 这一页的区块标题按平台分叉:macOS 用「配置状态」(SetupGuide 统一展示),
* Windows 才额外有「发送能力」卡。旧断言写死了「发送能力」,在 macOS 上恒假。
*
* `exact: true` 不能省:Windows 上同一页还有「发送能力授权」,
* 子串匹配会同时命中 —— 这正是 CI 上 strict mode violation 的来源。
*/
await expect(
page.getByRole('heading', {
name: process.platform === 'darwin' ? '配置状态' : '发送能力',
exact: true
})
).toBeVisible()
expect(pageErrors).toEqual([])
} finally {
@@ -82,59 +148,40 @@ test('SCHEDULED-REPORT-UI-01 opens the scheduled report dialog without viewport
}
})
test('SCHEDULED-REPORT-UI-02 keeps notification switch off when Agent Hub is offline', async () => {
test('SCHEDULED-REPORT-UI-02 keeps the notification switch off and explains why when Agent Hub is offline', async () => {
test.skip(
process.platform !== 'darwin' && process.platform !== 'win32',
'The scheduled report send capability requires macOS or Windows'
)
const fixture = await launchTestApp({ now: Date.parse('2026-08-27T08:30:00+08:00') })
const fixture = await launchTestApp({ now: FIXTURE_NOW })
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
const page = fixture.page
try {
await fixture.page.getByRole('button', { name: '日报' }).click()
await fixture.page.getByRole('tab', { name: '定时日报' }).click()
const notificationSwitch = fixture.page.getByRole('switch', { name: '微信异常通知' })
await expect(notificationSwitch).toHaveAttribute('aria-checked', 'false')
await notificationSwitch.click()
await openScheduledReportEditor(page)
// 异常通知挂在编辑器侧栏,是「定时日报」的全局能力,不是单条规则的配置。
await expect(page.getByRole('heading', { name: '微信异常通知' })).toBeVisible()
await expect(
fixture.page
.getByRole('alert')
.getByText('需要先连接 Agent Hub 微信机器人,才能接收异常通知。')
page.getByText('需要先连接 Agent Hub 微信机器人,才能接收异常通知。')
).toBeVisible()
/*
* Agent Hub 离线 ⇒ 开关**禁用**(不是"能点但点了没反应")。
* 迁移前这里是个点了会弹报错的开关;新实现把不可用前置成禁用 + 原因 + 去处,
* 所以断言的是禁用状态与原因文案,而不是点击后的错误提示。
*/
const notificationSwitch = page.getByRole('switch', { name: '开启微信异常通知' })
await expect(notificationSwitch).toHaveAttribute('aria-checked', 'false')
await fixture.page.getByRole('button', { name: '前往 Agent Hub' }).click()
await expect(
fixture.page.getByRole('heading', { name: 'Agent Hub', exact: true })
).toBeVisible()
} finally {
await fixture.close()
}
})
await expect(notificationSwitch).toBeDisabled()
test('SCHEDULED-REPORT-UI-03 sends a simulated error notification in debug mode', async () => {
test.skip(
(process.platform !== 'darwin' && process.platform !== 'win32') || !debugNotificationEnabled,
'The scheduled report debug action requires a supported platform and VITE_SCHEDULED_REPORT_DEBUG=true'
)
const fixture = await launchTestApp({ now: Date.parse('2026-08-27T08:30:00+08:00') })
try {
await fixture.page.getByRole('button', { name: '日报' }).click()
await fixture.page.getByRole('tab', { name: '定时日报' }).click()
await fixture.page
.getByRole('button', { name: /新建定时日报/ })
.first()
.click()
// 能力未就绪时不给「发送测试」—— 不给一个点了必然失败的按钮。
await expect(page.getByRole('button', { name: '发送测试' })).toHaveCount(0)
const dialog = fixture.page.getByRole('dialog', { name: '新建定时日报' })
await dialog.getByPlaceholder('例如:技术交流 · 每日日报').fill('调试定时日报')
await dialog.getByRole('button', { name: '创建定时日报' }).click()
await expect(dialog).toHaveCount(0)
await page.getByRole('button', { name: '去连接 Agent Hub' }).click()
await expect(page.getByRole('heading', { name: 'Agent Hub', exact: true })).toBeVisible()
const debugButton = fixture.page.getByRole('button', { name: '测试错误信息发送' })
await expect(debugButton).toBeVisible()
await debugButton.click()
await expect(
fixture.page.getByText('测试错误信息已发送到 Agent Hub 微信通知接收者')
).toBeVisible()
await expect(fixture.page.getByText('定时日报错误通知测试')).toBeVisible()
expect(pageErrors).toEqual([])
} finally {
await fixture.close()
}
+109 -2
View File
@@ -445,10 +445,106 @@ handle('automation:getStatus', () => ({
message: connected ? '个人微信发送能力已就绪' : '尚未绑定个人微信发送能力'
}
}))
/*
* 定时日报是 `ruleType === 'scheduled_report'` 的**真规则**:列表由
* `automation:listRules` 过滤得到,创建 / 更新走 `automation:createRule` /
* `automation:updateRule`。fixture 必须实现这组通道,否则保存后列表读不回来。
*/
const scheduledReportRules = []
let automationRuleSequence = 0
// 「微信异常通知」是全局开关,新旧两套通道共用同一份状态。
let scheduledReportNotificationEnabled = false
handle('automation:listRules', () => [
builtinDailyReportRule(),
structuredClone(leaveNotificationRule)
structuredClone(leaveNotificationRule),
...scheduledReportRules.map((rule) => structuredClone(rule))
])
handle('automation:createRule', (draft) => {
automationRuleSequence += 1
const rule = {
...draft,
id: `fixture-scheduled-rule-${automationRuleSequence}`,
createdAt: fixtureNowMs,
updatedAt: fixtureNowMs
}
scheduledReportRules.push(rule)
return structuredClone(rule)
})
// `updateRule` / `setRuleEnabled` 收的是**单个对象**(`{ id, draft }` / `{ id, enabled }`),
// 与 `createRule(draft)` / `deleteRule(id)` 的位置参数不同 —— preload 就是这么拼的。
handle('automation:updateRule', (input) => {
const { id, draft } = input || {}
const index = scheduledReportRules.findIndex((rule) => rule.id === id)
if (index < 0) return null
scheduledReportRules[index] = {
...scheduledReportRules[index],
...(draft || {}),
id,
updatedAt: fixtureNowMs
}
return structuredClone(scheduledReportRules[index])
})
handle('automation:deleteRule', (id) => {
const index = scheduledReportRules.findIndex((rule) => rule.id === id)
if (index < 0) return false
scheduledReportRules.splice(index, 1)
return true
})
handle('automation:setRuleEnabled', (input) => {
const { id, enabled } = input || {}
const index = scheduledReportRules.findIndex((rule) => rule.id === id)
if (index < 0) return null
scheduledReportRules[index] = {
...scheduledReportRules[index],
enabled: enabled === true,
updatedAt: fixtureNowMs
}
return structuredClone(scheduledReportRules[index])
})
/*
* 微信异常通知(定时日报的全局能力,不是单条规则的配置)。
*
* 能力判定与 `ScheduledReportService#checkNotificationCapability` 一致:**Agent Hub 离线
* 就是不可用**,且要带上那句可操作的原因。fixture 里 Hub 是 offline,所以这里如实回
* 不可用 —— 不为了让开关点得动而假装就绪。
*
* 这四个通道生产环境都有;不注册会被渲染层的降级兜底吞掉,页面看起来"正常",
* 但测的就不是真实文案了。
*/
handle('automation:getScheduledReportNotificationSettings', () => ({
enabled: scheduledReportNotificationEnabled
}))
handle('automation:getScheduledReportNotificationCapability', () =>
agentHubStatus().hub === 'online'
? { ready: true, recipient: 'wxid_fixture_self' }
: {
ready: false,
reason: 'agent_hub_offline',
error: '需要先连接 Agent Hub 微信机器人,才能接收异常通知。'
}
)
handle('automation:setScheduledReportNotificationEnabled', (enabled) => {
if (enabled && agentHubStatus().hub !== 'online') {
scheduledReportNotificationEnabled = false
return {
success: false,
data: { enabled: false },
reason: 'agent_hub_offline',
error: '需要先连接 Agent Hub 微信机器人,才能接收异常通知。'
}
}
scheduledReportNotificationEnabled = Boolean(enabled)
return { success: true, data: { enabled: scheduledReportNotificationEnabled } }
})
handle('automation:testScheduledReportErrorNotification', (ruleId) =>
scheduledReportRules.some((rule) => rule.id === ruleId)
? {
success: true,
data: { ruleId, notificationId: 'fixture-notification', sentAt: fixtureNowMs }
}
: { success: false, error: '未找到这条定时日报规则' }
)
handle('automation:listGroups', () => automationGroupOptions())
handle('automation:listExecutions', () => [])
handle('automation:clearExecutions', () => true)
@@ -520,7 +616,6 @@ const scheduledReportTasks =
: []
const scheduledReportExecutions = []
const generatedReports = []
let scheduledReportNotificationEnabled = false
const scheduledReportNextRun = (scheduleTime) => {
const [hours, minutes] = String(scheduleTime || '09:00')
.split(':')
@@ -1296,6 +1391,18 @@ handle('agent-hub:clearLogs', () => ({ success: true }))
handle('agent-hub:startLogin', () => ({ status: agentHubStatus() }))
handle('agent-hub:cancelLogin', () => ({ status: agentHubStatus() }))
handle('agent-hub:disconnect', () => ({ status: agentHubStatus() }))
/*
* Agent Hub 对话记录(本机 conversations.json 的只读回看)。
*
* fixture 里 Hub 是 offline、也没有任何收发记录,所以恒为空 —— 与
* `agent-hub:getStatus` 的 offline 状态保持一致。
*
* 通道**必须**注册:会话面板挂载即调用,缺 handler 会被渲染层记成 pageerror。
*/
handle('agent-hub:getConversations', () => [])
handle('agent-hub:getConversation', () => null)
handle('agent-hub:clearConversations', () => ({ success: true }))
handle('image:getConfig', () => ({
success: true,
configured: true,