feat: 完成 UI 基础层与核心页面迁移

This commit is contained in:
Wxw-Gu
2026-08-19 17:43:07 +08:00
parent a72ae49e2c
commit e9cc996ebb
62 changed files with 4279 additions and 5178 deletions
+215 -10
View File
@@ -4,6 +4,13 @@ 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[] = []
@@ -29,6 +36,7 @@ test('KEY-01 KEY-02 invalid key remains recoverable and valid key enters the app
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()
@@ -67,8 +75,13 @@ test('P2-01 P2-02 guided connection exposes safe diagnostics and completes all s
await fixture.page.getByRole('button', { name: '检查完成,继续' }).click()
await fixture.page.getByRole('button', { name: '我已准备好' }).click()
await fixture.page.getByRole('button', { name: '开始准备连接组件' }).click()
await expect(fixture.page.getByRole('button', { name: '微信已登录,验证连接' })).toBeEnabled()
await fixture.page.getByRole('button', { name: '微信已登录,验证连接' }).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()
@@ -114,9 +127,130 @@ test('NAV-01 NAV-02 every top-level page is unique and switchable', async () =>
}
})
test('API-01 masks, reveals, and confirms rotation of the local API token', async () => {
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 {
await fixture.setWindowContentSize({ width: 1000, height: 650 })
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()
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(pageErrors).toEqual([])
} 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.setWindowContentSize({ width: 1000, height: 650 })
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()
await expect(
dialog.locator('.personal-wechat-send-status strong').filter({ hasText: '个人微信已绑定' })
).toBeVisible()
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(650)
expect(pageErrors).toEqual([])
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 {
await fixture.setWindowContentSize({ width: 1000, height: 650 })
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 supported WeChat versions dialog is keyboard-safe and fits the viewport', 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.setWindowContentSize({ width: 1000, height: 650 })
await fixture.page
.getByRole('navigation', { name: '一级导航' })
.getByRole('button', { name: '设置' })
.click()
await fixture.page.getByRole('button', { name: '文字转语音' }).click()
const trigger = fixture.page.getByRole('button', { name: '查看支持版本' })
await expect(trigger).toBeVisible()
await trigger.click()
const dialog = fixture.page.getByRole('dialog', { name: '支持的微信版本' })
await expect(dialog).toBeVisible()
await expect(dialog.getByText('4.1.6.12')).toBeVisible()
await expect(dialog.getByText('4.1.11.53')).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(trigger).toBeFocused()
} 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.setWindowContentSize({ width: 1000, height: 650 })
await fixture.page.getByRole('button', { name: 'API' }).click()
await expect(fixture.page.getByText('API Token', { exact: true })).toBeVisible()
await expect(fixture.page.getByText('••••••••••••••••')).toBeVisible()
@@ -128,6 +262,30 @@ test('API-01 masks, reveals, and confirms rotation of the local API token', asyn
fixture.page.once('dialog', (dialog) => dialog.accept())
await fixture.page.getByRole('button', { name: '重新生成 Token' }).click()
await expect(fixture.page.getByText('Token 已生成')).toBeVisible()
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()
}
@@ -135,7 +293,10 @@ test('API-01 masks, reveals, and confirms rotation of the local API token', asyn
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 {
await fixture.setWindowContentSize({ width: 1000, height: 650 })
const navigation = fixture.page.getByRole('navigation', { name: '一级导航' })
await fixture.page.getByRole('button', { name: '联系人 (1)' }).click()
await fixture.page.getByText('文件传输助手', { exact: true }).click()
@@ -157,6 +318,10 @@ test('EXPORT-01 multi-chat selection stays local to export and forces HTML', asy
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()
@@ -165,6 +330,26 @@ test('EXPORT-01 multi-chat selection stays local to export and forces HTML', asy
}
})
test('LAYOUT-01 core workspaces fit a narrow desktop viewport without page errors', async () => {
const fixture = await launchTestApp()
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.setWindowContentSize({ width: 820, height: 600 })
const navigation = fixture.page.getByRole('navigation', { name: '一级导航' })
for (const pageName of ['档案', '问问微信', '日报', '导出', '设置']) {
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 {
@@ -173,9 +358,19 @@ test('ARCH-01 ARCH-02 folded chats and supported message types are represented e
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 } })
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()
@@ -291,11 +486,21 @@ test('REPORT-01 REPORT-02 generates a fixed report with non-empty local assets',
await expect(fixture.page.getByText('固定响应模型')).toBeVisible()
await expect(fixture.page.getByText('图片模型')).toBeVisible()
await expect(fixture.page.getByText('固定图片识别模型')).toBeVisible()
await expect(fixture.page.getByRole('button', { name: '生成微信卡片' })).toHaveCount(0)
await fixture.page.getByRole('button', { name: '更多' }).click()
await expect(fixture.page.getByRole('button', { name: '生成微信卡片' })).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()
await fixture.page.setViewportSize({ width: 1024, height: 760 })
await fixture.setWindowContentSize({ width: 1024, height: 760 })
const reportTitle = fixture.page.getByRole('heading', { name: '产品测试群 群聊日报' })
await expect(reportTitle).toBeVisible()
expect((await reportTitle.boundingBox())?.width || 0).toBeGreaterThan(170)
+78 -4
View File
@@ -12,9 +12,11 @@ app.setPath('logs', path.join(userData, 'logs'))
app.commandLine.appendSwitch('disable-gpu')
const VALID_KEY = 'a'.repeat(64)
const imageData =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Y9ZQmcAAAAASUVORK5CYII='
const imageData = `data:image/png;base64,${fs.readFileSync(path.join(root, 'resources/icon.png')).toString('base64')}`
const voiceData = 'UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQAAAAA='
const configuredNow = Number(process.env.WXE_E2E_NOW_MS)
const fixtureNowMs =
Number.isFinite(configuredNow) && configuredNow > 0 ? configuredNow : Date.now()
const formatFixtureDateTime = (timestampSeconds) => {
const date = new Date(timestampSeconds * 1000)
@@ -24,7 +26,7 @@ const formatFixtureDateTime = (timestampSeconds) => {
const allFixtureMessages = Object.values(fixture.messages).flat()
const latestFixtureTime = Math.max(...allFixtureMessages.map((message) => message.createTime || 0))
const fixtureTimeOffset = Math.floor(Date.now() / 1000) - 3600 - latestFixtureTime
const fixtureTimeOffset = Math.floor(fixtureNowMs / 1000) - 3600 - latestFixtureTime
for (const message of allFixtureMessages) {
message.createTime = (message.createTime || latestFixtureTime) + fixtureTimeOffset
message.datetime = formatFixtureDateTime(message.createTime)
@@ -206,7 +208,7 @@ let settings = {
debugEnabled: false,
autoLogin: connected,
autoLoginPreferenceSet: true,
appearanceTheme: 'light',
appearanceTheme: process.env.WXE_E2E_APPEARANCE_THEME === 'dark' ? 'dark' : 'light',
compactMode: false,
showStartupProgress: false,
imageXorKey: '0x40',
@@ -241,6 +243,65 @@ handle('settings:set', (patch) => {
settings = { ...settings, ...patch }
return { settings, settingsPath: path.join(userData, 'settings.json') }
})
handle('tts:getSettings', () => ({
success: true,
settings: {
provider: 'fish-audio',
hasApiKey: false,
hasStoredApiKey: false,
hasEnvironmentApiKey: false,
keySource: 'missing',
encryptionAvailable: true,
selectedVoiceId: '',
outputFormat: 'mp3',
model: 's2.1-pro-free',
phase: 'ready'
},
voices: []
}))
handle('wechat-personal:getRuntimeStatus', () => ({
version: 'v0.0.18',
state: 'ready',
downloadedBytes: 100,
totalBytes: 100,
progress: 1,
platform: 'darwin',
architecture: 'arm64',
supported: true,
removable: true
}))
handle('wechat-personal:getStatus', () => ({
state: 'online',
platform: 'darwin',
arch: 'arm64',
sipDisabled: true,
wechatRunning: true,
wechatPid: 4668,
boundWechatPid: 4668,
oneBotPid: 5401,
endpoint: '127.0.0.1:58080',
endpointReady: true,
wechatVersion: '4.1.11.53',
runtimeReady: true,
attachReady: true,
baseAddress: '0x114ef8000',
baseAddressReady: true,
textHookInstalled: true,
textHookReady: true,
imageHookInstalled: true,
imageHookReady: true,
messageListenerReady: true,
canSend: true,
canSendText: true,
canSendImage: true,
canSendVoice: true,
message: '个人微信已绑定'
}))
handle('wechat-share:getConfig', () => ({
success: true,
configured: true,
serviceUrl: 'https://share.example.test'
}))
handle('key:getSavedDbKey', () => ({
success: true,
key: savedKey || undefined,
@@ -579,6 +640,19 @@ handle('api:rotateToken', () => ({
maskedToken: '••••••••••••••••'
}))
handle('api:copyCurl', () => ({ success: true }))
handle('api:skillStatus', () => ({
available: true,
version: 'v1.2',
filePath: '/fixture/tracememo-reader/SKILL.md',
directoryPath: '/fixture/tracememo-reader',
source: 'development',
githubUrl: 'https://example.test/tracememo-reader'
}))
handle('api:readSkill', () => ({
success: true,
content:
'# TraceMemo Reader\n\n## 能力\n- 读取本地聊天记录\n- 导出群聊日报\n\n仅在用户授权后访问。'
}))
handle('api:start', () => ({ running: true, host: settings.apiHost, port: settings.apiPort }))
handle('api:stop', () => ({ running: false, host: settings.apiHost, port: settings.apiPort }))
handle('api:toggle', (enabled) => ({
+18 -1
View File
@@ -10,6 +10,7 @@ export interface TestApplication {
app: ElectronApplication
page: Page
userData: string
setWindowContentSize: (size: { width: number; height: number }) => Promise<void>
close: () => Promise<void>
}
@@ -20,6 +21,8 @@ export async function launchTestApp(
largeContacts?: number
corruptCache?: boolean
aiFailure?: string
now?: number
appearanceTheme?: 'light' | 'dark'
} = {}
): Promise<TestApplication> {
const ownsDirectory = !options.userData
@@ -39,15 +42,29 @@ export async function launchTestApp(
WXE_E2E_MODE: options.mode || 'connected',
WXE_E2E_LARGE_CONTACTS: String(options.largeContacts || 0),
WXE_E2E_CORRUPT_CACHE: options.corruptCache ? '1' : '0',
WXE_E2E_AI_FAILURE: options.aiFailure || ''
WXE_E2E_AI_FAILURE: options.aiFailure || '',
WXE_E2E_NOW_MS: options.now ? String(options.now) : '',
WXE_E2E_APPEARANCE_THEME: options.appearanceTheme || 'light'
}
})
const page = await app.firstWindow()
await page.waitForLoadState('domcontentloaded')
const setWindowContentSize = async (size: { width: number; height: number }): Promise<void> => {
await app.evaluate(({ BrowserWindow }, nextSize) => {
const [window] = BrowserWindow.getAllWindows()
if (!window) throw new Error('E2E BrowserWindow is unavailable')
window.setContentSize(nextSize.width, nextSize.height)
}, size)
await page.waitForFunction(
(nextSize) => window.innerWidth === nextSize.width && window.innerHeight === nextSize.height,
size
)
}
return {
app,
page,
userData,
setWindowContentSize,
close: async () => {
if (!page.isClosed() && closeDelayMs > 0) await page.waitForTimeout(closeDelayMs)
await app.close().catch(() => undefined)
+208 -3
View File
@@ -5,6 +5,14 @@ import { launchTestApp } from './support/electron'
const baselineDirectory = resolve(`tests/e2e/__screenshots__/${process.platform}/visual.spec.ts`)
const visualViewport = { width: 1000, height: 650 }
const visualNow = Date.parse('2026-08-19T14:46:40+08:00')
async function clearScreenshotFocus(page: import('@playwright/test').Page): Promise<void> {
await page.evaluate(() => {
if (document.activeElement instanceof HTMLElement) document.activeElement.blur()
})
}
test.skip(
!existsSync(baselineDirectory) && process.env.WXE_UPDATE_VISUAL_BASELINES !== '1',
`No reviewed ${process.platform} visual baseline is committed yet`
@@ -13,8 +21,9 @@ test.skip(
test('NAV-01 login page visual @visual', async () => {
const fixture = await launchTestApp({ mode: 'disconnected' })
try {
await fixture.page.setViewportSize(visualViewport)
await fixture.setWindowContentSize(visualViewport)
await expect(fixture.page.getByRole('heading', { name: 'TraceMemo(迹忆)' })).toBeVisible()
await clearScreenshotFocus(fixture.page)
await expect(fixture.page).toHaveScreenshot('login-page.png', {
animations: 'disabled',
caret: 'hide'
@@ -25,11 +34,12 @@ test('NAV-01 login page visual @visual', async () => {
})
test('ARCH-01 archive page visual @visual', async () => {
const fixture = await launchTestApp()
const fixture = await launchTestApp({ now: visualNow })
try {
await fixture.page.setViewportSize(visualViewport)
await fixture.setWindowContentSize(visualViewport)
await fixture.page.getByText('产品测试群', { exact: true }).click()
await expect(fixture.page.getByText('这是一条脱敏测试消息', { exact: true })).toBeVisible()
await clearScreenshotFocus(fixture.page)
await expect(fixture.page).toHaveScreenshot('archive-page.png', {
animations: 'disabled',
caret: 'hide'
@@ -38,3 +48,198 @@ test('ARCH-01 archive page visual @visual', async () => {
await fixture.close()
}
})
test('ASK-01 AI Search idle page visual @visual', async () => {
const fixture = await launchTestApp({ now: visualNow })
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.setWindowContentSize(visualViewport)
await fixture.page.getByRole('button', { name: '问问微信' }).click()
await expect(fixture.page.getByRole('heading', { name: '问问你的微信' })).toBeVisible()
await expect(fixture.page.getByPlaceholder(/例如:技术交流群/)).toBeVisible()
await expect(fixture.page.getByRole('main', { name: '问问微信' })).not.toBeEmpty()
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(pageErrors).toEqual([])
await clearScreenshotFocus(fixture.page)
await expect(fixture.page).toHaveScreenshot('ai-search-idle-page.png', {
animations: 'disabled',
caret: 'hide'
})
} finally {
await fixture.close()
}
})
test('ASK-03 AI Search result page visual @visual', async () => {
const fixture = await launchTestApp({ now: visualNow })
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.setWindowContentSize(visualViewport)
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 expect(fixture.page.getByRole('button', { name: /选择证据 E1/ })).toBeVisible()
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(pageErrors).toEqual([])
await clearScreenshotFocus(fixture.page)
await expect(fixture.page).toHaveScreenshot('ai-search-result-page.png', {
animations: 'disabled',
caret: 'hide'
})
} finally {
await fixture.close()
}
})
test('API-01 Reader Skill preview visual @visual', async () => {
const fixture = await launchTestApp({ now: visualNow })
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.setWindowContentSize(visualViewport)
await fixture.page.getByRole('button', { name: 'API' }).click()
await fixture.page
.locator('#api-reader-skill')
.getByRole('button', { name: '预览 Skill' })
.click()
await expect(
fixture.page.getByRole('dialog', { name: 'TraceMemo Reader Skill 预览' })
).toBeVisible()
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(pageErrors).toEqual([])
await clearScreenshotFocus(fixture.page)
await expect(fixture.page).toHaveScreenshot('api-skill-preview.png', {
animations: 'disabled',
caret: 'hide'
})
} finally {
await fixture.close()
}
})
test('CHAT-02 personal WeChat send dialog visual @visual', async () => {
test.skip(process.platform !== 'darwin', 'Personal WeChat sending is currently macOS-only')
const fixture = await launchTestApp({ now: visualNow })
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.setWindowContentSize(visualViewport)
await fixture.page.getByText('产品测试群', { exact: true }).click()
await fixture.page.getByRole('button', { name: '发送消息' }).click()
await expect(fixture.page.getByRole('dialog', { name: '个人微信测试发送' })).toBeVisible()
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(pageErrors).toEqual([])
await clearScreenshotFocus(fixture.page)
await expect(fixture.page).toHaveScreenshot('personal-wechat-send-dialog.png', {
animations: 'disabled',
caret: 'hide'
})
} finally {
await fixture.close()
}
})
test('CHAT-03 image viewer visual @visual', async () => {
const fixture = await launchTestApp({ now: visualNow })
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.setWindowContentSize(visualViewport)
await fixture.page.getByText('产品测试群', { exact: true }).click()
await fixture.page.getByRole('button', { name: '查看图片' }).click()
await expect(fixture.page.getByRole('dialog', { name: '图片查看' })).toBeVisible()
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(pageErrors).toEqual([])
await clearScreenshotFocus(fixture.page)
await expect(fixture.page).toHaveScreenshot('chat-image-viewer.png', {
animations: 'disabled',
caret: 'hide'
})
} finally {
await fixture.close()
}
})
test('EXPORT-01 export workspace idle visual @visual', async () => {
const fixture = await launchTestApp({ now: visualNow })
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.setWindowContentSize(visualViewport)
await fixture.page.getByRole('button', { name: '导出' }).click()
await expect(fixture.page.getByRole('heading', { name: '导出设置' })).toBeVisible()
await expect(fixture.page.getByRole('button', { name: '开始导出' })).toBeEnabled()
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(pageErrors).toEqual([])
await clearScreenshotFocus(fixture.page)
await expect(fixture.page).toHaveScreenshot('export-workspace-idle.png', {
animations: 'disabled',
caret: 'hide'
})
} finally {
await fixture.close()
}
})
test('THEME-01 archive page dark visual @visual', async () => {
const fixture = await launchTestApp({ now: visualNow, appearanceTheme: 'dark' })
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.setWindowContentSize(visualViewport)
await fixture.page.getByText('产品测试群', { exact: true }).click()
await expect(fixture.page.getByText('这是一条脱敏测试消息', { exact: true })).toBeVisible()
await expect(fixture.page.locator('html')).toHaveAttribute('data-theme', 'dark')
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(pageErrors).toEqual([])
await clearScreenshotFocus(fixture.page)
await expect(fixture.page).toHaveScreenshot('archive-page-dark.png', {
animations: 'disabled',
caret: 'hide'
})
} finally {
await fixture.close()
}
})
test('THEME-02 export workspace dark visual @visual', async () => {
const fixture = await launchTestApp({ now: visualNow, appearanceTheme: 'dark' })
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.setWindowContentSize(visualViewport)
await fixture.page.getByRole('button', { name: '导出' }).click()
await expect(fixture.page.getByRole('heading', { name: '导出设置' })).toBeVisible()
await expect(fixture.page.locator('html')).toHaveAttribute('data-theme', 'dark')
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(pageErrors).toEqual([])
await clearScreenshotFocus(fixture.page)
await expect(fixture.page).toHaveScreenshot('export-workspace-dark.png', {
animations: 'disabled',
caret: 'hide'
})
} finally {
await fixture.close()
}
})