Files
WechatExplorer/tests/e2e/app.spec.ts
T
电摇小子andWxw-Gu 3e57a8432d test: 建立桌面端自动化回归测试体系并完善跨平台 CI
(cherry picked from commit 74267ae2f63f8256c3da84b04f5b330e6e9d4c67)
2026-08-03 10:08:28 +08:00

290 lines
12 KiB
TypeScript

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'
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: 'WechatExplorer' })).toBeVisible()
await expect(fixture.page.getByRole('main')).not.toBeEmpty()
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))
const errorDialog = fixture.page.waitForEvent('dialog')
await fixture.page
.getByRole('button', { name: '连接数据库' })
.evaluate((element: HTMLButtonElement) => element.click())
const dialog = await errorDialog
expect(dialog.message()).toContain('数据库密钥无效')
await dialog.dismiss()
await expect(keyInput).toBeVisible()
await keyInput.fill('a'.repeat(64))
await fixture.page.getByRole('button', { name: '连接数据库' }).click()
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()
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('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()
await fixture.page.locator('.image-bubble.image-loaded').click()
await expect(fixture.page.getByText('图片查看', { exact: true })).toBeVisible()
await fixture.page.locator('.image-viewer-overlay').click({ position: { x: 5, y: 5 } })
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()
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
})
} 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()
await fixture.page.locator('.report-source-item').filter({ hasText: '产品测试群' }).click()
const generate = fixture.page.getByRole('button', { name: '开始生成日报' })
await expect(generate).toBeEnabled()
await generate.click()
await expect(fixture.page.getByAltText('产品测试群 群聊日报')).toBeVisible({
timeout: 15_000
})
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('button', { name: '开始生成日报' }).click()
await expect(fixture.page.getByText(/本地假服务错误 401/).first()).toBeVisible()
await expect(fixture.page.getByRole('button', { name: '重试' })).toBeEnabled()
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()
}
})