Files
WechatExplorer/tests/e2e/app.spec.ts
T

1265 lines
56 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 { expect, test } from '@playwright/test'
import { existsSync, mkdtempSync, readFileSync, rmSync, statSync } from 'fs'
import { tmpdir } from 'os'
import { resolve } from 'path'
import { launchTestApp } from './support/electron'
async function dismissFirstUseWelcome(page: import('@playwright/test').Page): Promise<void> {
const welcome = page.getByRole('dialog', { name: '开始探索你的微信' })
await expect(welcome).toBeVisible()
await welcome.getByRole('button', { name: '关闭' }).click()
await expect(welcome).toHaveCount(0)
}
test('APP-01 first launch renders a usable connection screen without uncaught errors', async () => {
const fixture = await launchTestApp({ mode: 'disconnected' })
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await expect(fixture.page.getByRole('heading', { name: 'TraceMemo(迹忆)' })).toBeVisible()
await expect(fixture.page.getByRole('main')).not.toBeEmpty()
const loginLayout = await fixture.page.locator('.database-login-page').evaluate((element) => ({
width: Math.round(element.getBoundingClientRect().width),
viewportWidth: window.innerWidth
}))
expect(loginLayout.width).toBe(loginLayout.viewportWidth)
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(pageErrors).toEqual([])
} finally {
await fixture.close()
}
})
test('KEY-01 KEY-02 invalid key remains recoverable and valid key enters the app', async () => {
const fixture = await launchTestApp({ mode: 'disconnected' })
try {
await fixture.page.getByRole('tab', { name: /高级用户/ }).click()
const keyInput = fixture.page.getByLabel('数据库密钥')
await keyInput.fill('b'.repeat(64))
await fixture.page.getByRole('button', { name: '连接数据库' }).click()
await expect(fixture.page.getByText('数据库密钥无效')).toBeVisible()
await expect(keyInput).toBeVisible()
await keyInput.fill('a'.repeat(64))
await fixture.page.getByRole('button', { name: '连接数据库' }).click()
await dismissFirstUseWelcome(fixture.page)
await expect(fixture.page.getByRole('navigation', { name: '一级导航' })).toBeVisible()
} finally {
await fixture.close()
}
})
test('P0-01 an invalid directory can be corrected and retried without restarting', async () => {
test.skip(process.platform !== 'win32', 'Manual database directory editing is Windows-only')
const fixture = await launchTestApp({ mode: 'disconnected' })
try {
await fixture.page.getByRole('tab', { name: /高级用户/ }).click()
await fixture.page.getByLabel('数据库密钥').fill('a'.repeat(64))
await fixture.page.getByLabel('微信数据目录').fill('Z:\\missing-wechat-data')
await fixture.page.getByRole('button', { name: '连接数据库' }).click()
await expect(fixture.page.getByText('微信数据目录不存在,请重新选择目录')).toBeVisible()
await expect(fixture.page.getByLabel('微信数据目录')).toBeEditable()
await expect(fixture.page.getByRole('button', { name: '选择目录' })).toBeEnabled()
await fixture.page.getByRole('button', { name: '选择目录' }).click()
await expect(fixture.page.getByLabel('微信数据目录')).toHaveValue('fixture-account')
await fixture.page.getByRole('button', { name: '连接数据库' }).click()
await dismissFirstUseWelcome(fixture.page)
await expect(fixture.page.getByRole('navigation', { name: '一级导航' })).toBeVisible()
} finally {
await fixture.close()
}
})
test('P2-01 P2-02 guided connection exposes safe diagnostics and completes all stages', async () => {
const fixture = await launchTestApp({ mode: 'disconnected' })
try {
await expect(fixture.page.getByText('4.1.9.57')).toBeVisible()
await expect(fixture.page.getByText('微信 4.x(WCDB)')).toBeVisible()
await expect(fixture.page.getByRole('button', { name: '复制脱敏诊断摘要' })).toBeEnabled()
await fixture.page.getByRole('button', { name: '检查完成,继续' }).click()
await fixture.page.getByRole('button', { name: '我已准备好' }).click()
// macOS(含 Apple Silicon)走「获取密钥」流程,Windows 仍是「准备连接组件」。
await fixture.page
.getByRole('button', {
name: process.platform === 'darwin' ? '开始获取密钥' : '开始准备连接组件'
})
.click()
const verifyConnection = fixture.page.getByRole('button', {
name: '验证连接',
exact: true
})
await expect(verifyConnection).toBeEnabled()
await verifyConnection.click()
await dismissFirstUseWelcome(fixture.page)
await expect(fixture.page.getByRole('navigation', { name: '一级导航' })).toBeVisible()
} finally {
await fixture.close()
}
})
test('KEY-03 changing one key does not invalidate archive data or unrelated settings', async () => {
const fixture = await launchTestApp()
try {
await expect(fixture.page.getByRole('navigation', { name: '一级导航' })).toBeVisible()
const result = await fixture.page.evaluate(async () => {
const before = await window.api.getContacts()
const image = await window.api.saveImageKeyConfig({
resourceRoot: 'fixture-account',
xorKey: '0x41',
aesKey: 'fedcba9876543210'
})
const after = await window.api.getContacts()
const database = await window.api.getSavedDbKey()
return { before, after, image, database }
})
expect(result.image.success).toBe(true)
expect(result.before).toEqual(result.after)
expect(result.database.saved).toBe(true)
} finally {
await fixture.close()
}
})
test('NAV-01 NAV-02 every top-level page is unique and switchable', async () => {
const fixture = await launchTestApp()
const labels = ['档案', '问问微信', '日报', '退群监控', 'Agent', '导出', 'API', '设置']
try {
const navigation = fixture.page.getByRole('navigation', { name: '一级导航' })
await expect(navigation).toBeVisible()
const appShell = fixture.page.locator('.app-shell')
await expect(appShell).toBeVisible()
const shellLayout = await appShell.evaluate((element) => ({
width: Math.round(element.getBoundingClientRect().width),
viewportWidth: window.innerWidth
}))
expect(shellLayout.width).toBe(shellLayout.viewportWidth)
for (const label of labels) {
await expect(navigation.getByRole('button', { name: label })).toHaveCount(1)
await navigation.getByRole('button', { name: label }).click()
await expect(fixture.page.locator(`main.app-shell-main[aria-label="${label}"]`)).toBeVisible()
}
} finally {
await fixture.close()
}
})
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[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.page.getByRole('button', { name: '退群监控' }).click()
await expect(fixture.page.getByRole('heading', { name: '退群监控', exact: true })).toBeVisible()
await expect(fixture.page.getByText('测试成员退出了产品测试群', { exact: true })).toBeVisible()
await expect(fixture.page.getByText(/240 人 → 239 人/)).toBeVisible()
await expect(fixture.page.getByText('实时监听已启用')).toBeVisible()
await expect(fixture.page.getByRole('button', { name: '立即检查' })).toBeEnabled()
await expect(fixture.page.getByRole('button', { name: '清空记录' })).toBeEnabled()
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(pageErrors).toEqual([])
} finally {
await fixture.close()
}
})
test('NAV-04 exit monitor management can save an empty scope and show the setup state', async () => {
const fixture = await launchTestApp()
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.getByText('发送能力已就绪', { exact: true })).toBeVisible()
await expect(fixture.page.getByRole('checkbox', { name: '监控产品测试群' })).toBeChecked()
await fixture.page.getByRole('checkbox', { name: '监控产品测试群' }).click()
await fixture.page.getByRole('checkbox', { name: '监控折叠群聊样本' }).click()
await fixture.page.getByRole('button', { name: '保存监控群聊' }).click()
await expect(fixture.page.getByText('还没有设置监控群聊', { exact: true })).toBeVisible()
await expect(fixture.page.getByRole('button', { name: '选择群聊' })).toBeVisible()
} finally {
await fixture.close()
}
})
/**
* 退群通知的**内容模板**已随「退群通知」一起迁进自动化,管理群聊页只负责
* 「监测哪些群」。这条用例锁住新家:模板可编辑、可保存,且真的落库。
*/
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[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
const conversationSearch = fixture.page.getByRole('searchbox', { name: '搜索会话' })
await conversationSearch.fill('产品')
await expect(fixture.page.getByText('产品测试群', { exact: true })).toBeVisible()
await conversationSearch.fill('')
await fixture.page.getByRole('button', { name: '刷新会话列表' }).click()
await fixture.page.getByText('产品测试群', { exact: true }).click()
const moreButton = fixture.page.getByRole('button', { name: '更多' })
await moreButton.click()
await expect(fixture.page.getByRole('menuitem', { name: '刷新数据' })).toBeVisible()
await fixture.page.keyboard.press('Escape')
await expect(fixture.page.getByRole('menuitem', { name: '刷新数据' })).toHaveCount(0)
await expect(moreButton).toBeFocused()
await moreButton.click()
await fixture.page.getByRole('menuitem', { name: '刷新数据' }).click()
await expect(fixture.page.getByRole('heading', { name: '产品测试群' })).toBeVisible()
await fixture.page.getByRole('button', { name: '搜索当前聊天' }).click()
const searchInput = fixture.page.getByRole('textbox', { name: '搜索当前聊天内容' })
await expect(searchInput).toBeFocused()
await searchInput.fill('测试')
await expect(searchInput).toHaveValue('测试')
expect(
await fixture.page
.locator('.chat-archive-header')
.evaluate((element) => element.scrollWidth <= element.clientWidth)
).toBe(true)
await fixture.page.getByRole('button', { name: '关闭搜索' }).click()
await expect(searchInput).toHaveCount(0)
const avatarSwitch = fixture.page.getByRole('switch', { name: '显示头像' })
await expect(avatarSwitch).toBeChecked()
await avatarSwitch.click()
await expect(avatarSwitch).not.toBeChecked()
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(pageErrors).toEqual([])
} finally {
await fixture.close()
}
})
test('CHAT-REALTIME-01 archive refreshes from a native message-shard event', async () => {
const fixture = await launchTestApp()
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.page.getByText('产品测试群', { exact: true }).click()
await expect(fixture.page.getByText('这是一条脱敏测试消息', { exact: true })).toBeVisible()
const result = await fixture.page.evaluate(() =>
window.electron.ipcRenderer.invoke('test:messageChange', {
md5: 'group-regular-md5',
event: { db: 'message_0.db', table: 'message', action: 'update' },
message: {
id: 'msg-monitor',
localId: 999,
from: 'user',
type: '普通文本',
datetime: '2026-08-31 12:00:00',
content: '消息',
isSender: false,
name: '测试成员',
senderId: 'wxid_fixture_member',
createTime: Math.floor(Date.now() / 1000),
contentData: { type: 'text', content: '消息' }
}
})
)
expect(result).toEqual({ success: true })
await expect(fixture.page.getByText('消息', { exact: true })).toBeVisible({
timeout: 3_000
})
expect(pageErrors).toEqual([])
} finally {
await fixture.close()
}
})
test('CHAT-REALTIME-02 refreshes and reorders conversations without losing selection or filter', async () => {
const fixture = await launchTestApp({ realtimeReorder: true })
try {
const names = fixture.page.locator('.conversation-item-name')
await expect(names).toHaveText(['产品测试群', '自动刷新 B', '自动刷新 A'])
await fixture.page.getByRole('button', { name: /自动刷新 A/ }).click()
await fixture.page.getByRole('searchbox', { name: '搜索会话' }).fill('自动刷新')
await expect(fixture.page.getByRole('searchbox', { name: '搜索会话' })).toHaveValue('自动刷新')
await expect(fixture.page.locator('.conversation-item-name')).toHaveText([
'自动刷新 B',
'自动刷新 A',
'自动刷新 D',
'自动刷新 C'
])
const readsBeforeMessage = await fixture.page.evaluate(() =>
window.electron.ipcRenderer.invoke('test:getContactReadCount')
)
await fixture.page.evaluate(() =>
window.electron.ipcRenderer.invoke('test:messageChange', {
reorderContactMd5: 'realtime-group-a-md5',
event: { db: 'message_0.db', table: 'message', action: 'insert' }
})
)
await expect
.poll(async () => fixture.page.locator('.conversation-item-name').allTextContents())
.toEqual(['自动刷新 A', '自动刷新 B', '自动刷新 D', '自动刷新 C'])
expect(await fixture.page.getByRole('searchbox', { name: '搜索会话' }).inputValue()).toBe(
'自动刷新'
)
await expect(
fixture.page.locator('.conversation-item.active .conversation-item-name')
).toHaveText('自动刷新 A')
const readsAfterSingleMessage = await fixture.page.evaluate(() =>
window.electron.ipcRenderer.invoke('test:getContactReadCount')
)
expect(readsAfterSingleMessage - readsBeforeMessage).toBe(1)
await Promise.all(
Array.from({ length: 50 }, () =>
fixture.page.evaluate(() =>
window.electron.ipcRenderer.invoke('test:messageChange', {
reorderContactMd5: 'realtime-direct-c-md5',
event: { db: 'message_0.db', table: 'message', action: 'insert' }
})
)
)
)
await expect
.poll(async () => fixture.page.locator('.conversation-item-name').allTextContents())
.toEqual(['自动刷新 A', '自动刷新 B', '自动刷新 C', '自动刷新 D'])
const readsAfterBurst = await fixture.page.evaluate(() =>
window.electron.ipcRenderer.invoke('test:getContactReadCount')
)
expect(readsAfterBurst - readsAfterSingleMessage).toBe(1)
await expect(fixture.page.locator('.conversation-item-name')).toHaveCount(4)
} finally {
await fixture.close()
}
})
test('CHAT-02 personal WeChat send dialog is keyboard-safe and fits the viewport', async () => {
test.skip(process.platform !== 'darwin', 'Personal WeChat sending is currently macOS-only')
const fixture = await launchTestApp()
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.page.getByText('产品测试群', { exact: true }).click()
const trigger = fixture.page.getByRole('button', { name: '文字转语音' })
await trigger.click()
const dialog = fixture.page.getByRole('dialog', { name: '产品测试群' })
await expect(dialog).toBeVisible()
const startSending = dialog.getByRole('button', { name: '开始发送' })
if (await startSending.isVisible()) await startSending.click()
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(await dialog.evaluate((element) => element.scrollWidth <= element.clientWidth)).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)
)
expect(pageErrors).toEqual([])
const voiceText = dialog.getByRole('textbox', { name: '语音文字' })
await voiceText.focus()
await expect(voiceText).toBeFocused()
expect(await dialog.evaluate((element) => element.scrollWidth <= element.clientWidth)).toBe(
true
)
await fixture.page.keyboard.press('Escape')
await expect(dialog).toHaveCount(0)
await expect(trigger).toBeFocused()
} finally {
await fixture.close()
}
})
test('GUIDE-01 first-use welcome is keyboard-safe and fits the viewport', async () => {
const fixture = await launchTestApp()
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
const guideButton = fixture.page.getByRole('button', { name: '新手引导' })
await guideButton.click()
const dialog = fixture.page.getByRole('dialog', { name: '开始探索你的微信' })
await expect(dialog).toBeVisible()
await expect(dialog.getByRole('button', { name: /试试 AI 群聊日报/ })).toBeVisible()
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(pageErrors).toEqual([])
await fixture.page.keyboard.press('Escape')
await expect(dialog).toHaveCount(0)
await expect(guideButton).toBeFocused()
} finally {
await fixture.close()
}
})
test('SETTINGS-01 macOS WeChat setup keeps the binding guidance focused', async () => {
test.skip(process.platform !== 'darwin', 'The personal WeChat runtime is currently macOS-only')
const fixture = await launchTestApp()
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.page
.getByRole('navigation', { name: '一级导航' })
.getByRole('button', { name: '设置' })
.click()
await fixture.page.getByRole('button', { name: '微信发送' }).click()
await expect(
fixture.page.getByText('请保持微信未登录窗口状态,点击“绑定微信”后,再点击微信窗口登录。')
).toBeVisible()
await expect(fixture.page.getByRole('button', { name: '支持版本' })).toHaveCount(0)
await expect(fixture.page.getByText('查看支持的微信版本')).toHaveCount(0)
await expect(fixture.page.getByText('高级诊断')).toHaveCount(0)
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(pageErrors).toEqual([])
await fixture.page.getByRole('button', { name: '文字转语音' }).click()
const modelSelect = fixture.page.getByRole('combobox', { name: '合成模型' })
await modelSelect.scrollIntoViewIfNeeded()
await modelSelect.click()
await expect(fixture.page.getByRole('option', { name: 's2.1-pro', exact: true })).toBeVisible()
await fixture.page.keyboard.press('Escape')
await expect(modelSelect).toBeFocused()
await expect(fixture.page.getByRole('searchbox', { name: '按音色名称搜索' })).toBeDisabled()
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(pageErrors).toEqual([])
} finally {
await fixture.close()
}
})
test('SETTINGS-02 basic settings controls fit the default desktop window and keep their semantics', async () => {
const fixture = await launchTestApp()
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.page
.getByRole('navigation', { name: '一级导航' })
.getByRole('button', { name: '设置' })
.click()
await expect(fixture.page.getByRole('heading', { name: '账号与数据库' })).toBeVisible()
await expect(fixture.page.getByRole('button', { name: '存储与导出' })).toHaveCount(0)
const autoLoginSwitch = fixture.page.getByRole('switch', { name: '启动时自动连接数据库' })
await expect(autoLoginSwitch).toBeVisible()
await expect(autoLoginSwitch).toBeChecked()
await autoLoginSwitch.click()
await expect(autoLoginSwitch).not.toBeChecked()
await autoLoginSwitch.click()
await expect(autoLoginSwitch).toBeChecked()
await fixture.page.getByRole('button', { name: '缓存与清理' }).click()
await expect(fixture.page.getByRole('heading', { name: '缓存与清理' })).toBeVisible()
await expect(fixture.page.getByRole('button', { name: '清理全部' })).toBeEnabled()
await fixture.page.getByRole('button', { name: '高级' }).click()
const debugSwitch = fixture.page.getByRole('switch', { name: '显示诊断日志' })
await expect(debugSwitch).toBeVisible()
await debugSwitch.click()
await expect(debugSwitch).toBeChecked()
// 防撤回已下线:设置导航不再暴露该入口(与「存储与导出」同一处理方式)。
await expect(fixture.page.getByRole('button', { name: '防撤回' })).toHaveCount(0)
await fixture.page.getByRole('button', { name: '关于' }).click()
await expect(fixture.page.getByRole('button', { name: '检查更新' })).toBeVisible()
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(pageErrors).toEqual([])
} finally {
await fixture.close()
}
})
test('UPDATE-01 simulated startup update navigates to live progress and never exits on install', async () => {
const fixture = await launchTestApp({ updateSimulation: true })
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
const dialog = fixture.page.getByRole('alertdialog', { name: '发现新版本 v2.0.0' })
await expect(dialog).toBeVisible()
await dialog.getByRole('button', { name: '立即下载' }).click()
await expect(fixture.page.getByRole('heading', { name: '关于' })).toBeVisible()
await expect(fixture.page.getByRole('button', { name: '关于' })).toHaveClass(/active/)
await expect(fixture.page.getByText('正在下载 v2.0.0')).toBeVisible()
await expect
.poll(async () => {
const value = await fixture.page
.getByRole('progressbar', { name: /更新下载进度/ })
.getAttribute('aria-valuenow')
return Number(value)
})
.toBeGreaterThan(0)
await expect(fixture.page.getByText(/MB \/ 60\.0 MB/)).toBeVisible()
await expect(fixture.page.getByText(/MB\/s/)).toBeVisible()
await expect(fixture.page.getByText('v2.0.0 已准备完成')).toBeVisible({ timeout: 5_000 })
await fixture.page.getByRole('button', { name: '立即重启更新' }).click()
await expect(
fixture.page
.getByRole('region', { name: 'Notifications (F8)' })
.getByText('开发模拟模式:更新安装动作已模拟,未实际退出应用。')
).toBeVisible()
await expect(fixture.page.getByRole('heading', { name: '关于' })).toBeVisible()
expect(pageErrors).toEqual([])
} finally {
await fixture.close()
}
})
test('UPDATE-02 unsigned macOS update opens the latest release without downloading', async () => {
const fixture = await launchTestApp({ unsignedMacUpdate: true })
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
const dialog = fixture.page.getByRole('alertdialog', { name: '发现新版本 v2.2.3' })
await expect(dialog).toBeVisible()
await expect(dialog.getByText('有新版本可用,前往 GitHub 下载最新版本。')).toBeVisible()
await dialog.getByRole('button', { name: '前往下载' }).click()
const openedUrl = await fixture.page.evaluate(() =>
window.electron.ipcRenderer.invoke('app-update:getOpenedDownloadUrl')
)
expect(openedUrl).toBe('https://github.com/Wxw-Gu/TraceMemo/releases/latest')
await expect(fixture.page.getByRole('progressbar')).toHaveCount(0)
expect(pageErrors).toEqual([])
} finally {
await fixture.close()
}
})
test('SETTINGS-03 database key actions keep destructive confirmation keyboard-safe', async () => {
const fixture = await launchTestApp()
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
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: '数据库密钥' })).toBeVisible()
await expect(fixture.page.getByRole('button', { name: '重新验证' })).toBeVisible()
const clearKey = fixture.page.getByRole('button', { name: '清除密钥' })
await clearKey.scrollIntoViewIfNeeded()
await clearKey.click()
const dialog = fixture.page.getByRole('alertdialog', { name: '确认清除数据库密钥?' })
await expect(dialog).toBeVisible()
await fixture.page.keyboard.press('Escape')
await expect(dialog).toHaveCount(0)
await expect(clearKey).toBeFocused()
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(pageErrors).toEqual([])
} finally {
await fixture.close()
}
})
test('SETTINGS-04 image decryption controls keep filtering and selection keyboard-safe', async () => {
const fixture = await launchTestApp()
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
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()
const filterGroup = fixture.page.getByRole('radiogroup', { name: '会话类型筛选' })
await filterGroup.scrollIntoViewIfNeeded()
const allFilter = filterGroup.getByRole('radio', { name: '全部 3' })
await expect(allFilter).toBeChecked()
await allFilter.focus()
await fixture.page.keyboard.press('ArrowRight')
const groupFilter = filterGroup.getByRole('radio', { name: '群聊 2' })
await expect(groupFilter).toBeFocused()
await fixture.page.keyboard.press('Space')
await expect(groupFilter).toBeChecked()
await fixture.page.getByRole('searchbox', { name: '搜索会话' }).fill('产品')
const conversationSelect = fixture.page.getByRole('combobox', { name: '选择会话' })
await conversationSelect.click()
await expect(fixture.page.getByRole('option', { name: '产品测试群' })).toBeVisible()
await expect(fixture.page.getByRole('option', { name: '折叠群聊样本' })).toHaveCount(0)
await fixture.page.keyboard.press('Escape')
await expect(conversationSelect).toBeFocused()
await conversationSelect.click()
await fixture.page.getByRole('option', { name: '产品测试群' }).click()
await expect(conversationSelect).toContainText('产品测试群')
const testAction = fixture.page.getByRole('button', { name: '测试图片解析' })
await expect(testAction).toBeEnabled()
await testAction.click()
await expect(fixture.page.getByText('图片可以读取')).toBeVisible()
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(
await fixture.page
.locator('.settings-page-scroll')
.evaluate((element) => element.scrollWidth <= element.clientWidth)
).toBe(true)
expect(pageErrors).toEqual([])
} finally {
await fixture.close()
}
})
test('SETTINGS-05 AI model editor keeps provider and capability semantics', async () => {
const fixture = await launchTestApp()
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.page
.getByRole('navigation', { name: '一级导航' })
.getByRole('button', { name: '设置' })
.click()
await fixture.page.getByRole('button', { name: 'AI 模型' }).click()
await expect(fixture.page.getByRole('heading', { name: 'AI 模型' })).toBeVisible()
await expect(fixture.page.getByRole('button', { name: '当前默认' })).toBeDisabled()
await fixture.page.getByRole('button', { name: '编辑' }).click()
await expect(fixture.page.getByRole('heading', { name: '编辑供应商' })).toBeVisible()
await expect(fixture.page.getByLabel('供应商名称')).toBeFocused()
await expect(fixture.page.locator('#ai-provider-editor')).toBeInViewport()
await fixture.page.getByRole('button', { name: '关闭' }).click()
await fixture.page.getByRole('button', { name: '添加供应商' }).click()
await expect(fixture.page.getByRole('heading', { name: '新增供应商' })).toBeVisible()
await expect(fixture.page.getByLabel('供应商名称')).toBeFocused()
await fixture.page.getByLabel('供应商 ID').fill('fixture-new-provider')
const presetSelect = fixture.page.getByRole('combobox', { name: '快速模板' })
await presetSelect.click()
await fixture.page.getByRole('option', { name: 'OpenAI', exact: true }).click()
await expect(fixture.page.getByLabel('供应商名称')).toHaveValue('OpenAI')
await expect(fixture.page.getByLabel('模型名称')).toHaveValue('gpt-4o-mini')
const providerType = fixture.page.getByRole('combobox', { name: '供应商类型' })
await providerType.click()
await expect(fixture.page.getByRole('option', { name: 'Anthropic Messages' })).toBeVisible()
await fixture.page.keyboard.press('Escape')
await expect(providerType).toBeFocused()
const vision = fixture.page.getByRole('checkbox', { name: '图片理解 gpt-4o-mini' })
const ocr = fixture.page.getByRole('checkbox', { name: '图片文字识别 gpt-4o-mini' })
await vision.click()
await expect(vision).toBeChecked()
await expect(ocr).toBeChecked()
await expect(fixture.page.getByRole('radio', { name: '默认 gpt-4o-mini' })).toBeChecked()
await fixture.page.getByText('高级设置', { exact: true }).click()
await expect(fixture.page.getByLabel('请求超时(ms)')).toHaveValue('120000')
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(
await fixture.page
.locator('.settings-page-scroll')
.evaluate((element) => element.scrollWidth <= element.clientWidth)
).toBe(true)
expect(pageErrors).toEqual([])
} finally {
await fixture.close()
}
})
test('SETTINGS-07 voice recognition controls keep selection semantics at the default desktop size', async () => {
const fixture = await launchTestApp()
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
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: '语音转文字' })).toBeVisible()
const categoryTabs = fixture.page.getByRole('tablist', { name: '会话类别' })
await categoryTabs.scrollIntoViewIfNeeded()
await expect(fixture.page.locator('.voice-conversation-avatar img')).toHaveCount(1)
await expect(categoryTabs.getByRole('tab', { name: /群聊/ })).toHaveAttribute(
'data-state',
'active'
)
const conversation = fixture.page.getByRole('checkbox', { name: /产品测试群/ })
await conversation.click()
await expect(conversation).toBeChecked()
const rangeSelect = fixture.page.getByRole('combobox', { name: '语音转写时间范围' })
await rangeSelect.click()
await expect(fixture.page.getByRole('option', { name: '所选会话全部历史' })).toBeVisible()
await fixture.page.keyboard.press('Escape')
await expect(rangeSelect).toBeFocused()
await expect(fixture.page.getByRole('button', { name: '开始转写' })).toBeDisabled()
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(
await fixture.page
.locator('.settings-page-scroll')
.evaluate((element) => element.scrollWidth <= element.clientWidth)
).toBe(true)
expect(pageErrors).toEqual([])
} finally {
await fixture.close()
}
})
test('AGENT-01 Agent Hub controls stay usable in the default offline layout', async () => {
const fixture = await launchTestApp()
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.page.getByRole('button', { name: 'Agent' }).click()
await expect(fixture.page.getByRole('heading', { name: 'Agent Hub' })).toBeVisible()
await expect(fixture.page.getByText('Agent Hub 未运行')).toBeVisible()
await expect(fixture.page.getByRole('button', { name: '扫码登录微信机器人' })).toBeDisabled()
const logSource = fixture.page.getByRole('combobox', { name: '筛选日志来源' })
await logSource.scrollIntoViewIfNeeded()
await logSource.click()
await expect(fixture.page.getByRole('option', { name: '微信连接器' })).toBeVisible()
await fixture.page.keyboard.press('Escape')
await expect(logSource).toBeFocused()
await expect(fixture.page.getByRole('button', { name: '复制日志' })).toBeDisabled()
await fixture.page.getByRole('button', { name: '清空', exact: true }).click()
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(
await fixture.page
.locator('.agent-hub-workspace')
.evaluate((element) => element.scrollWidth <= element.clientWidth)
).toBe(true)
expect(pageErrors).toEqual([])
} finally {
await fixture.close()
}
})
test('API-01 manages the local API token and previews the Reader Skill safely', async () => {
const fixture = await launchTestApp()
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.page.getByRole('button', { name: 'API' }).click()
await expect(fixture.page.getByText('API Token', { exact: true })).toBeVisible()
await expect(fixture.page.getByText('••••••••••••••••')).toBeVisible()
await expect(fixture.page.getByText('fixture-api-token')).toHaveCount(0)
await fixture.page.getByRole('button', { name: '显示 Token' }).click()
await expect(fixture.page.getByText('fixture-api-token')).toBeVisible()
fixture.page.once('dialog', (dialog) => dialog.accept())
await fixture.page.getByRole('button', { name: '重新生成 Token' }).click()
await expect(fixture.page.getByText('Token 已生成')).toBeVisible()
const moreTrigger = fixture.page
.locator('#api-reader-skill')
.getByRole('button', { name: '更多' })
await moreTrigger.click()
await expect(fixture.page.getByRole('menuitem', { name: '打开本地文件夹' })).toBeVisible()
await fixture.page.keyboard.press('Escape')
await expect(fixture.page.getByRole('menu')).toHaveCount(0)
await expect(moreTrigger).toBeFocused()
const previewTrigger = fixture.page
.locator('#api-reader-skill')
.getByRole('button', { name: '预览 Skill' })
await expect(previewTrigger).toBeEnabled()
await previewTrigger.click()
const previewDialog = fixture.page.getByRole('dialog', {
name: 'TraceMemo Reader Skill 预览'
})
await expect(previewDialog).toBeVisible()
await expect(previewDialog.getByRole('heading', { name: '能力' })).toBeVisible()
await previewDialog.getByRole('button', { name: '原始文本' }).click()
await expect(previewDialog.getByText(/# TraceMemo Reader/)).toBeVisible()
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(
await previewDialog.evaluate((element) => element.scrollWidth <= element.clientWidth)
).toBe(true)
expect(pageErrors).toEqual([])
await fixture.page.keyboard.press('Escape')
await expect(previewDialog).toHaveCount(0)
await expect(previewTrigger).toBeFocused()
} finally {
await fixture.close()
}
})
test('EXPORT-01 multi-chat selection stays local to export and forces HTML', async () => {
const fixture = await launchTestApp()
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
const navigation = fixture.page.getByRole('navigation', { name: '一级导航' })
await fixture.page.getByRole('button', { name: '联系人 (1)' }).click()
await fixture.page.getByText('文件传输助手', { exact: true }).click()
await expect(fixture.page.getByText('转发多条内容', { exact: true })).toBeVisible()
await navigation.getByRole('button', { name: '导出' }).click()
const contactList = fixture.page.locator('.export-contact-list')
await expect(contactList.getByRole('button', { name: /文件传输助手/ })).toHaveAttribute(
'aria-pressed',
'true'
)
await fixture.page.getByRole('button', { name: '+ 添加聊天' }).click()
await contactList.getByRole('button', { name: /产品测试群/ }).click()
await expect(fixture.page.getByText('已选 2 / 5 个')).toBeVisible()
await expect(fixture.page.getByRole('radio', { name: 'CSV' })).toBeDisabled()
const htmlFormat = fixture.page
.getByRole('radiogroup', { name: '导出格式' })
.getByRole('radio', { name: /HTML/ })
await expect(htmlFormat).toBeChecked()
await expect(fixture.page.getByText('文件传输助手、产品测试群 · 共 2 个聊天')).toBeVisible()
await expect(
fixture.page.locator('.export-preview-bubble').filter({ hasText: '这是一条脱敏测试消息' })
).toHaveCount(1)
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(pageErrors).toEqual([])
await navigation.getByRole('button', { name: '档案' }).click()
await expect(fixture.page.getByText('转发多条内容', { exact: true })).toBeVisible()
} finally {
await fixture.close()
}
})
test('EXPORT-02 large contact list stays bounded and searchable', async () => {
const fixture = await launchTestApp({ largeContacts: 1500 })
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.page.getByRole('button', { name: '导出' }).click()
await expect(fixture.page.getByText('共 1,503 个')).toBeVisible()
const contactList = fixture.page.locator('.export-contact-list')
expect(await contactList.getByRole('button').count()).toBeLessThan(50)
const compositeButtons = [
fixture.page.getByRole('button', { name: /^全部导出/ }),
fixture.page
.getByRole('radiogroup', { name: '导出格式' })
.getByRole('radio', { name: /HTML/ }),
fixture.page.locator('.export-workspace > aside:first-child > button:last-child')
]
for (const button of compositeButtons) {
await expect(button).toBeVisible()
expect(
await button.evaluate(
(element) => element.scrollHeight <= element.clientHeight && element.clientHeight > 32
)
).toBe(true)
}
const previewGeometryIsStable = await fixture.page
.locator('.export-workspace > aside:last-child')
.evaluate((previewPanel) => {
const scrollRegion = previewPanel.children.item(1)
const statistics = previewPanel.children.item(2)
if (!(scrollRegion instanceof HTMLElement) || !(statistics instanceof HTMLElement)) {
return false
}
const panelRect = previewPanel.getBoundingClientRect()
const scrollRect = scrollRegion.getBoundingClientRect()
const statisticsRect = statistics.getBoundingClientRect()
return (
scrollRect.bottom <= statisticsRect.top &&
statisticsRect.bottom <= panelRect.bottom &&
statistics.scrollHeight <= statistics.clientHeight
)
})
expect(previewGeometryIsStable).toBe(true)
await fixture.page.getByRole('textbox', { name: '搜索聊天' }).fill('性能样本 1499')
const target = contactList.getByRole('button', { name: /性能样本 1499/ })
await expect(target).toBeVisible()
await target.click()
await expect(target).toHaveAttribute('aria-pressed', 'true')
expect(pageErrors).toEqual([])
} finally {
await fixture.close()
}
})
test('LAYOUT-01 core workspaces fit the default desktop window without page errors', async () => {
const fixture = await launchTestApp()
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
const navigation = fixture.page.getByRole('navigation', { name: '一级导航' })
for (const pageName of ['档案', '问问微信', '日报', '导出', 'API', '设置']) {
await navigation.getByRole('button', { name: pageName }).click()
await expect(fixture.page.locator('main.app-shell-main')).not.toBeEmpty()
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
}
expect(pageErrors).toEqual([])
} finally {
await fixture.close()
}
})
test('ARCH-01 ARCH-02 folded chats and supported message types are represented explicitly', async () => {
const fixture = await launchTestApp()
try {
await expect(fixture.page.getByText('产品测试群', { exact: true })).toBeVisible()
await fixture.page.getByText('产品测试群', { exact: true }).click()
await expect(fixture.page.getByText('这是一条脱敏测试消息', { exact: true })).toBeVisible()
await expect(fixture.page.getByText('暂不支持此消息', { exact: true })).toBeVisible()
await expect(fixture.page.getByAltText('图片')).toBeVisible()
const imageTrigger = fixture.page.getByRole('button', { name: '查看图片' })
await imageTrigger.click()
const imageDialog = fixture.page.getByRole('dialog', { name: '图片查看' })
await expect(imageDialog).toBeVisible()
await imageDialog.getByRole('button', { name: '放大' }).click()
await expect(imageDialog.getByText('110%')).toBeVisible()
await imageDialog.getByRole('button', { name: '右旋转' }).click()
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
await fixture.page.keyboard.press('Escape')
await expect(imageDialog).toHaveCount(0)
await expect(imageTrigger).toBeFocused()
await fixture.page.getByRole('button', { name: '折叠群聊 (1)' }).click()
await expect(fixture.page.getByText('折叠群聊样本', { exact: true })).toBeVisible()
} finally {
await fixture.close()
}
})
test('MEDIA-01 and merged forwards work on the first interaction', async () => {
const fixture = await launchTestApp()
try {
await fixture.page.getByRole('button', { name: '联系人 (1)' }).click()
await fixture.page.getByText('文件传输助手', { exact: true }).click()
await expect(fixture.page.getByText('转发多条内容', { exact: true })).toBeVisible()
await fixture.page.evaluate(() => {
Object.defineProperty(window, '__wxePlayCount', {
configurable: true,
value: 0,
writable: true
})
HTMLMediaElement.prototype.play = async function () {
;(window as Window & { __wxePlayCount: number }).__wxePlayCount += 1
}
HTMLMediaElement.prototype.pause = function () {
return undefined
}
HTMLMediaElement.prototype.load = function () {
return undefined
}
})
await fixture.page.locator('.voice-message').click()
await expect(fixture.page.locator('.voice-icon')).toHaveClass(/playing/)
expect(
await fixture.page.evaluate(
() => (window as Window & { __wxePlayCount: number }).__wxePlayCount
)
).toBe(1)
} finally {
await fixture.close()
}
})
test('MEDIA-02 MEDIA-04 return accurate unsupported and HTTP 403 reasons', async () => {
const fixture = await launchTestApp()
try {
const result = await fixture.page.evaluate(async () => ({
image: await window.api.getImage('unsupported'),
sticker: await window.api.getSticker(
'https://fixture.invalid/403?token=secret',
'b'.repeat(32)
)
}))
expect(result.image).toMatchObject({ success: false, error: '不支持的 DAT 版本' })
expect(result.sticker).toMatchObject({
success: false,
failureCode: 'access_denied',
httpStatus: 403
})
} finally {
await fixture.close()
}
})
test('ASK-01 uses the local fixed AI service and keeps evidence in the UI', 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.getByPlaceholder(/例如:技术交流群/).fill('测试群讨论了什么?')
await fixture.page.getByRole('button', { name: '开始分析' }).click()
await expect(fixture.page.getByText(/固定假回答:测试数据中的核心流程正常/)).toBeVisible({
timeout: 15_000
})
await fixture.page.getByRole('button', { name: /历史提问/ }).click()
await fixture.page.getByRole('button', { name: '测试群讨论了什么?', exact: true }).click()
const toastClose = fixture.page.getByRole('button', { name: '关闭通知' })
await expect(toastClose).toBeVisible()
expect(await toastClose.boundingBox()).toMatchObject({ width: 28, height: 28 })
await toastClose.click()
await expect(toastClose).toHaveCount(0)
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(pageErrors).toEqual([])
} finally {
await fixture.close()
}
})
test('ASK-02 AI failures are recoverable and do not break the archive', async () => {
const fixture = await launchTestApp({ aiFailure: '429' })
try {
await fixture.page.getByRole('button', { name: '问问微信' }).click()
await fixture.page.getByPlaceholder(/例如:技术交流群/).fill('测试')
await fixture.page.getByRole('button', { name: '开始分析' }).click()
await expect(fixture.page.getByText(/本地假服务错误 429/)).toBeVisible()
await fixture.page.getByRole('button', { name: '档案' }).click()
await expect(fixture.page.getByText('产品测试群', { exact: true })).toBeVisible()
} finally {
await fixture.close()
}
})
test('REPORT-01 REPORT-02 generates a fixed report with non-empty local assets', async () => {
const fixture = await launchTestApp()
try {
await fixture.page.getByRole('button', { name: '日报' }).click()
await fixture.page.getByRole('button', { name: '开始生成日报' }).click()
await expect(fixture.page.getByRole('heading', { name: '生成群聊日报' })).toBeVisible()
const textModel = fixture.page.getByRole('combobox', { name: '文字总结模型' })
const visionModel = fixture.page.getByRole('combobox', { name: '图片理解模型' })
await expect(textModel).toContainText('固定响应模型')
await expect(visionModel).toContainText('固定图片识别模型')
await textModel.click()
await expect(fixture.page.getByRole('option')).toHaveCount(2)
await fixture.page.keyboard.press('Escape')
await expect(textModel).toBeFocused()
await visionModel.click()
await expect(fixture.page.getByRole('option')).toHaveCount(1)
await fixture.page.keyboard.press('Escape')
await expect(visionModel).toBeFocused()
await fixture.page.locator('.report-source-item').filter({ hasText: '产品测试群' }).click()
await fixture.page.getByRole('radio', { name: '近 7 天' }).click()
const generate = fixture.page.getByRole('button', { name: '开始生成日报' })
await expect(generate).toBeEnabled()
await generate.click()
await expect(fixture.page.getByAltText('产品测试群 群聊日报')).toBeVisible({
timeout: 15_000
})
await expect(fixture.page.getByText('文字模型')).toBeVisible()
await expect(fixture.page.getByText('固定响应模型')).toBeVisible()
await expect(fixture.page.getByText('图片模型')).toBeVisible()
await expect(fixture.page.getByText('固定图片识别模型')).toBeVisible()
await expect(fixture.page.getByRole('menuitem', { name: '生成微信卡片' })).toHaveCount(0)
const moreButton = fixture.page.getByRole('button', { name: '更多' })
await moreButton.click()
await fixture.page.getByRole('menuitem', { name: '生成微信卡片' }).click()
const shareDialog = fixture.page.getByRole('dialog', { name: '生成微信分享卡片' })
await expect(shareDialog).toBeVisible()
await expect(shareDialog.locator('input').first()).toHaveValue(/产品测试群日报/)
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
await fixture.page.keyboard.press('Escape')
await expect(shareDialog).toHaveCount(0)
await expect(moreButton).toBeFocused()
const reportTitle = fixture.page.getByRole('heading', { name: '产品测试群 群聊日报' })
await expect(reportTitle).toBeVisible()
expect((await reportTitle.boundingBox())?.width || 0).toBeGreaterThan(170)
await expect(fixture.page.getByRole('button', { name: '放大' })).toBeEnabled()
await fixture.page.getByRole('button', { name: '放大' }).click()
const exported = await fixture.page.evaluate(async () =>
window.api.exportGroupReport({
report: {} as never,
metadata: {} as never,
templateId: 'v1'
})
)
expect(exported.success).toBe(true)
expect(exported.imageDataUrl).toMatch(/^data:image\/png;base64,/)
expect(existsSync(exported.htmlPath!)).toBe(true)
expect(existsSync(exported.pngPath!)).toBe(true)
expect(statSync(exported.pngPath!).size).toBeGreaterThan(20)
} finally {
await fixture.close()
}
})
test('REPORT-03 report failure is retryable and leaves other pages usable', async () => {
const fixture = await launchTestApp({ aiFailure: '401' })
try {
await fixture.page.getByRole('button', { name: '日报' }).click()
await fixture.page.getByRole('button', { name: '开始生成日报' }).click()
await fixture.page.locator('.report-source-item').filter({ hasText: '产品测试群' }).click()
await fixture.page.getByRole('radio', { name: '近 7 天' }).click()
await fixture.page.getByRole('button', { name: '开始生成日报' }).click()
await expect(fixture.page.getByText(/本地假服务错误 401/).first()).toBeVisible()
await expect(fixture.page.getByRole('button', { name: '使用所选模型重新生成' })).toBeEnabled()
await expect(fixture.page.getByText(/从第三步继续/)).toBeVisible()
await fixture.page.getByRole('button', { name: '档案' }).click()
await expect(fixture.page.locator('main.app-shell-main[aria-label="档案"]')).toBeVisible()
await expect(
fixture.page.locator('.conversation-item-name').filter({ hasText: '产品测试群' }).first()
).toBeVisible()
} finally {
await fixture.close()
}
})
test('CACHE-01 corrupt startup cache degrades to native fixture data', async () => {
const fixture = await launchTestApp({ corruptCache: true })
try {
await expect(fixture.page.getByRole('navigation', { name: '一级导航' })).toBeVisible()
await expect(fixture.page.getByText('产品测试群', { exact: true })).toBeVisible()
} finally {
await fixture.close()
}
})
test('PERF-01 repeated startup with 1500 sessions remains bounded and responsive', async () => {
test.setTimeout(60_000)
const userData = mkdtempSync(resolve(tmpdir(), 'wxe-e2e-perf-'))
try {
for (let run = 0; run < 2; run += 1) {
const startedAt = Date.now()
const fixture = await launchTestApp({ userData, largeContacts: 1500 })
try {
await expect(fixture.page.getByRole('navigation', { name: '一级导航' })).toBeVisible({
timeout: 10_000
})
expect(Date.now() - startedAt).toBeLessThan(10_000)
await fixture.page
.getByRole('navigation', { name: '一级导航' })
.getByRole('button', { name: '设置' })
.click()
await expect(fixture.page.locator('main.app-shell-main[aria-label="设置"]')).toBeVisible()
} finally {
await fixture.close()
}
}
} finally {
rmSync(userData, { recursive: true, force: true })
}
})
test('KEY-04 e2e diagnostic log does not contain a supplied key', async () => {
const fixture = await launchTestApp()
const key = 'c'.repeat(64)
try {
await fixture.page.evaluate(
(databaseKey) =>
window.api.writeAppLog({
level: 'error',
scope: 'key-test',
message: `fixture key=${databaseKey}`
}),
key
)
const logPath = resolve(fixture.userData, 'logs/e2e.log')
const content = readFileSync(logPath, 'utf8')
expect(content).not.toContain(key)
} finally {
await fixture.close()
}
})