mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-10-04 03:01:42 +08:00
test: 完善 2.3.0 跨平台测试与 CI 稳定性
- 修复 Windows 与 macOS 单元测试环境差异\n- 完善组件、集成与 Electron E2E 测试\n- 修复异步等待、平台路径和环境依赖问题\n- 统一 E2E 窗口尺寸与 Visual 测试策略\n- 提升 GitHub Actions 跨平台 CI 稳定性
This commit is contained in:
@@ -31,10 +31,9 @@ jobs:
|
||||
cache-dependency-path: services/wechat-connector/go.sum
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
run: pnpm install
|
||||
|
||||
- name: Install Playwright Chromium
|
||||
if: runner.os == 'macOS'
|
||||
run: pnpm exec playwright install chromium
|
||||
|
||||
- name: Type check
|
||||
@@ -64,6 +63,8 @@ jobs:
|
||||
WXE_E2E_CLOSE_DELAY_MS: 0
|
||||
|
||||
- name: Platform visual regression
|
||||
# GitHub-hosted Windows 无法稳定容纳项目统一的 1400×800 Electron 窗口,Windows visual regression 改由可控 Windows 桌面环境执行。
|
||||
if: matrix.os != 'windows-latest'
|
||||
run: pnpm exec playwright test tests/e2e/visual.spec.ts
|
||||
env:
|
||||
WXE_E2E_CLOSE_DELAY_MS: 0
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
# monitor-automation
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tracememo",
|
||||
"version": "2.2.3",
|
||||
"version": "2.3.0",
|
||||
"packageManager": "pnpm@7.33.7",
|
||||
"description": "TraceMemo(迹忆)是一款本地优先、可追溯的 AI 微信知识与分析工作台。 原名 WechatExplorer,支持聊天记录搜索、知识库、微信群聊总结和 Agent 助手。",
|
||||
"keywords": [
|
||||
|
||||
@@ -7,8 +7,16 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func setTestHome(t *testing.T) string {
|
||||
t.Helper()
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("USERPROFILE", home)
|
||||
return home
|
||||
}
|
||||
|
||||
func TestSaveCredentialsKeepsOnlyLatestAccount(t *testing.T) {
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
setTestHome(t)
|
||||
old := &Credentials{ILinkBotID: "bot-old@im.bot", BotToken: "old-token"}
|
||||
latest := &Credentials{ILinkBotID: "bot-new@im.bot", BotToken: "new-token"}
|
||||
if err := SaveCredentials(old); err != nil {
|
||||
@@ -37,8 +45,7 @@ func TestSaveCredentialsKeepsOnlyLatestAccount(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAccountsDirUsesTraceMemoIdentity(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
home := setTestHome(t)
|
||||
dir, err := AccountsDir()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -50,8 +57,7 @@ func TestAccountsDirUsesTraceMemoIdentity(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLoadAllCredentialsFallsBackToLegacyDirectory(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
setTestHome(t)
|
||||
legacyDir, err := LegacyAccountsDir()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -308,6 +308,8 @@ function PersonalWechatMacSendDialog({
|
||||
<>
|
||||
<Dialog open onOpenChange={(open) => !open && handleClose()}>
|
||||
<DialogContent
|
||||
aria-label={displayName}
|
||||
aria-labelledby={undefined}
|
||||
className="personal-wechat-send-dialog max-h-[calc(100vh-2rem)] max-w-[720px] gap-0 overflow-y-auto p-0"
|
||||
onOpenAutoFocus={() => {
|
||||
restoreFocusRef.current =
|
||||
|
||||
@@ -170,6 +170,8 @@ export function PersonalWechatWindowsSendDialog({
|
||||
return (
|
||||
<Dialog open onOpenChange={(open) => !open && handleClose()}>
|
||||
<DialogContent
|
||||
aria-label={displayName}
|
||||
aria-labelledby={undefined}
|
||||
className="personal-wechat-send-dialog max-h-[calc(100vh-2rem)] max-w-[620px] gap-4 overflow-y-auto p-[22px]"
|
||||
onOpenAutoFocus={() => {
|
||||
restoreFocusRef.current =
|
||||
|
||||
@@ -65,12 +65,12 @@ export function ReportViewer({
|
||||
const viewportRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
setSendDialogOpen(false)
|
||||
const frame = window.requestAnimationFrame(() => {
|
||||
setStatus('')
|
||||
setImageError('')
|
||||
setIsSwitchingTemplate(false)
|
||||
setShareDialogOpen(false)
|
||||
setSendDialogOpen(false)
|
||||
setZoom(1)
|
||||
setFitZoom(1)
|
||||
setNaturalSize(null)
|
||||
|
||||
@@ -224,7 +224,7 @@ function ScheduledReportDialog({
|
||||
React.useEffect(() => {
|
||||
if (!open) return
|
||||
setKeyword('')
|
||||
setSelectedId(task?.target || task?.group || groups[0]?.md5 || '')
|
||||
setSelectedId(task?.target || task?.group || '')
|
||||
setName(task?.name || '')
|
||||
setScheduleTime(task?.scheduleTime || '09:00')
|
||||
const persistedRange = task?.reportRange
|
||||
@@ -236,7 +236,13 @@ function ScheduledReportDialog({
|
||||
setRangeMessages([])
|
||||
setMemberCount(null)
|
||||
setMemberCountAvailable(true)
|
||||
}, [groups, open, task])
|
||||
}, [open, task])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (open && !selectedId && groups[0]) {
|
||||
setSelectedId(groups[0].md5)
|
||||
}
|
||||
}, [groups, open, selectedId])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!open || !selected) return
|
||||
|
||||
+10
@@ -10,9 +10,16 @@
|
||||
--border-color: #dcdcdc;
|
||||
}
|
||||
|
||||
html {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
font-family: var(--wxex-font);
|
||||
background-color: var(--wxex-bg-app);
|
||||
color: var(--wxex-text-primary);
|
||||
@@ -21,7 +28,10 @@ body {
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
||||
@@ -290,9 +290,11 @@ describe('daily report controls', () => {
|
||||
|
||||
const retryButton = await screen.findByRole('button', { name: '使用所选模型重新生成' })
|
||||
const retryModel = screen.getByRole('combobox', { name: '切换模型' })
|
||||
await waitFor(() => expect(retryModel).toBeEnabled())
|
||||
await user.click(retryModel)
|
||||
expect(screen.getByRole('option', { name: '备用服务 · 备用模型' })).toBeVisible()
|
||||
await user.click(screen.getByRole('option', { name: '备用服务 · 备用模型' }))
|
||||
const fallbackModel = await screen.findByRole('option', { name: '备用服务 · 备用模型' })
|
||||
expect(fallbackModel).toBeVisible()
|
||||
await user.click(fallbackModel)
|
||||
fireEvent.click(retryButton)
|
||||
expect(onRetry).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ providerId: 'provider-2', model: 'model-2' })
|
||||
@@ -577,7 +579,7 @@ describe('daily report controls', () => {
|
||||
expect(onReveal).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('opens the current group send dialog with the report PNG preselected', async () => {
|
||||
it('opens the current group send dialog with the report PNG ready to send', async () => {
|
||||
const report: GeneratedReportRecord = {
|
||||
id: 'report-send',
|
||||
contactId: groupContact.md5,
|
||||
@@ -611,9 +613,9 @@ describe('daily report controls', () => {
|
||||
expect(await screen.findByRole('dialog', { name: '测试群' })).toBeVisible()
|
||||
const detect = screen.queryByRole('button', { name: '重新检测' })
|
||||
if (detect) fireEvent.click(detect)
|
||||
expect(screen.getByRole('radio', { name: '图片' })).toHaveAttribute('aria-checked', 'true')
|
||||
expect(screen.getByText('测试群日报.png')).toBeVisible()
|
||||
expect(screen.getByText('/Users/fixture/测试群日报.png')).toBeVisible()
|
||||
expect(screen.getByRole('region', { name: '日报图片发送' })).toBeVisible()
|
||||
expect(screen.getByRole('button', { name: '发送日报图片' })).toBeEnabled()
|
||||
expect(screen.getByRole('region', { name: '日报图片发送' })).toHaveTextContent('测试群日报.png')
|
||||
})
|
||||
|
||||
it('disables current group sending outside macOS with a readable hover hint', () => {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 71 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 72 KiB |
+29
-32
@@ -16,9 +16,13 @@ test('APP-01 first launch renders a usable connection screen without uncaught er
|
||||
const pageErrors: Error[] = []
|
||||
fixture.page.on('pageerror', (error) => pageErrors.push(error))
|
||||
try {
|
||||
await fixture.setWindowContentSize({ width: 820, height: 600 })
|
||||
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)
|
||||
@@ -63,6 +67,7 @@ test('P0-01 an invalid directory can be corrected and retried without restarting
|
||||
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()
|
||||
@@ -121,6 +126,13 @@ test('NAV-01 NAV-02 every top-level page is unique and switchable', async () =>
|
||||
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()
|
||||
@@ -136,7 +148,6 @@ test('NAV-03 exit monitor page shows a member departure event and stays within t
|
||||
const pageErrors: Error[] = []
|
||||
fixture.page.on('pageerror', (error) => pageErrors.push(error))
|
||||
try {
|
||||
await fixture.setWindowContentSize({ width: 820, height: 600 })
|
||||
await fixture.page.getByRole('button', { name: '退群监控' }).click()
|
||||
await expect(fixture.page.getByRole('heading', { name: '退群监控', exact: true })).toBeVisible()
|
||||
await expect(fixture.page.getByText('测试成员退出了产品测试群', { exact: true })).toBeVisible()
|
||||
@@ -185,7 +196,6 @@ test('CHAT-01 archive More menu is keyboard-safe and keeps the page usable', asy
|
||||
const pageErrors: Error[] = []
|
||||
fixture.page.on('pageerror', (error) => pageErrors.push(error))
|
||||
try {
|
||||
await fixture.setWindowContentSize({ width: 820, height: 600 })
|
||||
const conversationSearch = fixture.page.getByRole('searchbox', { name: '搜索会话' })
|
||||
await conversationSearch.fill('产品')
|
||||
await expect(fixture.page.getByText('产品测试群', { exact: true })).toBeVisible()
|
||||
@@ -234,7 +244,6 @@ test('CHAT-REALTIME-01 archive refreshes from a native message-shard event', asy
|
||||
const pageErrors: Error[] = []
|
||||
fixture.page.on('pageerror', (error) => pageErrors.push(error))
|
||||
try {
|
||||
await fixture.setWindowContentSize({ width: 820, height: 600 })
|
||||
await fixture.page.getByText('产品测试群', { exact: true }).click()
|
||||
await expect(fixture.page.getByText('这是一条脱敏测试消息', { exact: true })).toBeVisible()
|
||||
|
||||
@@ -273,7 +282,6 @@ test('CHAT-02 personal WeChat send dialog is keyboard-safe and fits the viewport
|
||||
const pageErrors: Error[] = []
|
||||
fixture.page.on('pageerror', (error) => pageErrors.push(error))
|
||||
try {
|
||||
await fixture.setWindowContentSize({ width: 820, height: 600 })
|
||||
await fixture.page.getByText('产品测试群', { exact: true }).click()
|
||||
const trigger = fixture.page.getByRole('button', { name: '文字转语音' })
|
||||
await trigger.click()
|
||||
@@ -290,16 +298,14 @@ test('CHAT-02 personal WeChat send dialog is keyboard-safe and fits the viewport
|
||||
const bounds = await dialog.boundingBox()
|
||||
expect(bounds).not.toBeNull()
|
||||
expect(bounds!.y).toBeGreaterThanOrEqual(0)
|
||||
expect(bounds!.y + bounds!.height).toBeLessThanOrEqual(600)
|
||||
expect(bounds!.y + bounds!.height).toBeLessThanOrEqual(
|
||||
await fixture.page.evaluate(() => window.innerHeight)
|
||||
)
|
||||
expect(pageErrors).toEqual([])
|
||||
|
||||
const imageMode = dialog.getByRole('radio', { name: '图片' })
|
||||
const voiceMode = dialog.getByRole('radio', { name: '语音', exact: true })
|
||||
await imageMode.focus()
|
||||
await fixture.page.keyboard.press('ArrowRight')
|
||||
await expect(voiceMode).toBeFocused()
|
||||
await fixture.page.keyboard.press('Space')
|
||||
await expect(voiceMode).toBeChecked()
|
||||
const voiceText = dialog.getByRole('textbox', { name: '语音文字' })
|
||||
await voiceText.focus()
|
||||
await expect(voiceText).toBeFocused()
|
||||
expect(await dialog.evaluate((element) => element.scrollWidth <= element.clientWidth)).toBe(
|
||||
true
|
||||
)
|
||||
@@ -317,7 +323,6 @@ test('GUIDE-01 first-use welcome is keyboard-safe and fits the viewport', async
|
||||
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: '开始探索你的微信' })
|
||||
@@ -342,7 +347,6 @@ test('SETTINGS-01 supported WeChat versions dialog is keyboard-safe and fits the
|
||||
const pageErrors: Error[] = []
|
||||
fixture.page.on('pageerror', (error) => pageErrors.push(error))
|
||||
try {
|
||||
await fixture.setWindowContentSize({ width: 820, height: 600 })
|
||||
await fixture.page
|
||||
.getByRole('navigation', { name: '一级导航' })
|
||||
.getByRole('button', { name: '设置' })
|
||||
@@ -382,12 +386,11 @@ test('SETTINGS-01 supported WeChat versions dialog is keyboard-safe and fits the
|
||||
}
|
||||
})
|
||||
|
||||
test('SETTINGS-02 basic settings controls fit a narrow viewport and keep their semantics', async () => {
|
||||
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.setWindowContentSize({ width: 820, height: 600 })
|
||||
await fixture.page
|
||||
.getByRole('navigation', { name: '一级导航' })
|
||||
.getByRole('button', { name: '设置' })
|
||||
@@ -397,6 +400,9 @@ test('SETTINGS-02 basic settings controls fit a narrow viewport and keep their s
|
||||
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()
|
||||
|
||||
@@ -453,7 +459,9 @@ test('UPDATE-01 simulated startup update navigates to live progress and never ex
|
||||
await expect(fixture.page.getByText('v2.0.0 已准备完成')).toBeVisible({ timeout: 5_000 })
|
||||
await fixture.page.getByRole('button', { name: '立即重启更新' }).click()
|
||||
await expect(
|
||||
fixture.page.getByText('开发模拟模式:更新安装动作已模拟,未实际退出应用。')
|
||||
fixture.page
|
||||
.getByRole('region', { name: 'Notifications (F8)' })
|
||||
.getByText('开发模拟模式:更新安装动作已模拟,未实际退出应用。')
|
||||
).toBeVisible()
|
||||
await expect(fixture.page.getByRole('heading', { name: '关于' })).toBeVisible()
|
||||
expect(pageErrors).toEqual([])
|
||||
@@ -488,7 +496,6 @@ test('SETTINGS-03 database key actions keep destructive confirmation keyboard-sa
|
||||
const pageErrors: Error[] = []
|
||||
fixture.page.on('pageerror', (error) => pageErrors.push(error))
|
||||
try {
|
||||
await fixture.setWindowContentSize({ width: 820, height: 600 })
|
||||
await fixture.page
|
||||
.getByRole('navigation', { name: '一级导航' })
|
||||
.getByRole('button', { name: '设置' })
|
||||
@@ -519,7 +526,6 @@ test('SETTINGS-04 image decryption controls keep filtering and selection keyboar
|
||||
const pageErrors: Error[] = []
|
||||
fixture.page.on('pageerror', (error) => pageErrors.push(error))
|
||||
try {
|
||||
await fixture.setWindowContentSize({ width: 820, height: 600 })
|
||||
await fixture.page
|
||||
.getByRole('navigation', { name: '一级导航' })
|
||||
.getByRole('button', { name: '设置' })
|
||||
@@ -573,7 +579,6 @@ test('SETTINGS-05 AI model editor keeps provider and capability semantics', asyn
|
||||
const pageErrors: Error[] = []
|
||||
fixture.page.on('pageerror', (error) => pageErrors.push(error))
|
||||
try {
|
||||
await fixture.setWindowContentSize({ width: 820, height: 600 })
|
||||
await fixture.page
|
||||
.getByRole('navigation', { name: '一级导航' })
|
||||
.getByRole('button', { name: '设置' })
|
||||
@@ -628,12 +633,11 @@ test('SETTINGS-05 AI model editor keeps provider and capability semantics', asyn
|
||||
}
|
||||
})
|
||||
|
||||
test('SETTINGS-07 voice recognition controls keep selection semantics at a narrow viewport', async () => {
|
||||
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.setWindowContentSize({ width: 820, height: 600 })
|
||||
await fixture.page
|
||||
.getByRole('navigation', { name: '一级导航' })
|
||||
.getByRole('button', { name: '设置' })
|
||||
@@ -672,12 +676,11 @@ test('SETTINGS-07 voice recognition controls keep selection semantics at a narro
|
||||
}
|
||||
})
|
||||
|
||||
test('AGENT-01 Agent Hub controls stay usable in the offline narrow layout', async () => {
|
||||
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.setWindowContentSize({ width: 820, height: 600 })
|
||||
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()
|
||||
@@ -711,7 +714,6 @@ test('API-01 manages the local API token and previews the Reader Skill safely',
|
||||
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()
|
||||
@@ -766,7 +768,6 @@ test('EXPORT-01 multi-chat selection stays local to export and forces HTML', asy
|
||||
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()
|
||||
@@ -808,7 +809,6 @@ test('EXPORT-02 large contact list stays bounded and searchable', async () => {
|
||||
const pageErrors: Error[] = []
|
||||
fixture.page.on('pageerror', (error) => pageErrors.push(error))
|
||||
try {
|
||||
await fixture.setWindowContentSize({ width: 1400, height: 772 })
|
||||
await fixture.page.getByRole('button', { name: '导出' }).click()
|
||||
await expect(fixture.page.getByText('共 1,503 个')).toBeVisible()
|
||||
|
||||
@@ -861,12 +861,11 @@ test('EXPORT-02 large contact list stays bounded and searchable', async () => {
|
||||
}
|
||||
})
|
||||
|
||||
test('LAYOUT-01 core workspaces fit a narrow desktop viewport without page errors', async () => {
|
||||
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 {
|
||||
await fixture.setWindowContentSize({ width: 820, height: 600 })
|
||||
const navigation = fixture.page.getByRole('navigation', { name: '一级导航' })
|
||||
for (const pageName of ['档案', '问问微信', '日报', '导出', 'API', '设置']) {
|
||||
await navigation.getByRole('button', { name: pageName }).click()
|
||||
@@ -970,7 +969,6 @@ test('ASK-01 uses the local fixed AI service and keeps evidence in the UI', asyn
|
||||
const pageErrors: Error[] = []
|
||||
fixture.page.on('pageerror', (error) => pageErrors.push(error))
|
||||
try {
|
||||
await fixture.setWindowContentSize({ width: 820, height: 600 })
|
||||
await fixture.page.getByRole('button', { name: '问问微信' }).click()
|
||||
await fixture.page.getByPlaceholder(/例如:技术交流群/).fill('测试群讨论了什么?')
|
||||
await fixture.page.getByRole('button', { name: '开始分析' }).click()
|
||||
@@ -1053,7 +1051,6 @@ test('REPORT-01 REPORT-02 generates a fixed report with non-empty local assets',
|
||||
await expect(shareDialog).toHaveCount(0)
|
||||
await expect(moreButton).toBeFocused()
|
||||
|
||||
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)
|
||||
|
||||
@@ -44,6 +44,25 @@ const zipDirectory = async (
|
||||
})
|
||||
}
|
||||
|
||||
const extractZip = (zipPath: string, extractedDir: string): void => {
|
||||
if (process.platform === 'win32') {
|
||||
const escapePowerShellLiteral = (value: string): string => value.replace(/'/g, "''")
|
||||
execFileSync(
|
||||
'powershell.exe',
|
||||
[
|
||||
'-NoProfile',
|
||||
'-NonInteractive',
|
||||
'-Command',
|
||||
`Expand-Archive -LiteralPath '${escapePowerShellLiteral(zipPath)}' -DestinationPath '${escapePowerShellLiteral(extractedDir)}'`
|
||||
],
|
||||
{ windowsHide: true }
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
execFileSync('unzip', ['-q', zipPath, '-d', extractedDir])
|
||||
}
|
||||
|
||||
test('EXPORT-ARCHIVE-00 shows a loading state while archive data is still loading', async ({
|
||||
page
|
||||
}, testInfo) => {
|
||||
@@ -149,7 +168,7 @@ test('EXPORT-ARCHIVE-01 merged v2 archive is usable offline on desktop and mobil
|
||||
const extractedDir = join(fixtureRoot, 'extracted')
|
||||
await zipDirectory(outputDir, zipPath, '合并聊天档案')
|
||||
mkdirSync(extractedDir, { recursive: true })
|
||||
execFileSync('unzip', ['-q', zipPath, '-d', extractedDir])
|
||||
extractZip(zipPath, extractedDir)
|
||||
const offlineIndex = join(extractedDir, '合并聊天档案', 'index.html')
|
||||
|
||||
await page.setViewportSize({ width: 1440, height: 900 })
|
||||
|
||||
@@ -16,7 +16,6 @@ test('SCHEDULED-REPORT-UI-01 opens the scheduled report dialog without viewport
|
||||
const pageErrors: Error[] = []
|
||||
fixture.page.on('pageerror', (error) => pageErrors.push(error))
|
||||
try {
|
||||
await fixture.setWindowContentSize({ width: 820, height: 600 })
|
||||
await fixture.page.getByRole('button', { name: '日报' }).click()
|
||||
await fixture.page.getByRole('tab', { name: '定时日报' }).click()
|
||||
await expect(fixture.page.getByRole('heading', { name: '定时日报', exact: true })).toBeVisible()
|
||||
@@ -52,8 +51,8 @@ test('SCHEDULED-REPORT-UI-01 opens the scheduled report dialog without viewport
|
||||
await selects.nth(1).click()
|
||||
const memberOption = fixture.page.getByRole('option', { name: '微信昵称' })
|
||||
await expect(memberOption).toBeVisible()
|
||||
await memberOption.click()
|
||||
await expect(selects.nth(1)).toContainText('微信昵称')
|
||||
await memberOption.click({ force: true })
|
||||
await expect(selects.nth(1)).toContainText('微信昵称', { timeout: 8000 })
|
||||
|
||||
expect(
|
||||
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
|
||||
@@ -61,7 +60,9 @@ test('SCHEDULED-REPORT-UI-01 opens the scheduled report dialog without viewport
|
||||
const bounds = await dialog.boundingBox()
|
||||
expect(bounds).not.toBeNull()
|
||||
expect(bounds!.y).toBeGreaterThanOrEqual(0)
|
||||
expect(bounds!.y + bounds!.height).toBeLessThanOrEqual(600)
|
||||
expect(bounds!.y + bounds!.height).toBeLessThanOrEqual(
|
||||
await fixture.page.evaluate(() => window.innerHeight)
|
||||
)
|
||||
|
||||
await dialog.getByPlaceholder('例如:技术交流 · 每日日报').fill('E2E 定时日报')
|
||||
await dialog.getByRole('radio', { name: '今日' }).click()
|
||||
|
||||
@@ -7,6 +7,7 @@ const root = path.resolve(__dirname, '../../..')
|
||||
const fixture = structuredClone(require(path.join(root, 'tests/fixtures/chat-data.json')))
|
||||
const userData = process.env.WXE_E2E_USER_DATA
|
||||
if (!userData) throw new Error('WXE_E2E_USER_DATA is required')
|
||||
const backgroundE2E = process.env.WXE_E2E_HEADLESS === '1'
|
||||
app.setPath('userData', userData)
|
||||
app.setPath('logs', path.join(userData, 'logs'))
|
||||
app.commandLine.appendSwitch('disable-gpu')
|
||||
@@ -253,6 +254,7 @@ const groupExitEvent = {
|
||||
detectedAt: fixtureNowMs - 60 * 1000
|
||||
}
|
||||
let groupExitMonitorState = {
|
||||
enabled: connected,
|
||||
events: [groupExitEvent],
|
||||
running: connected,
|
||||
nativeMonitorActive: connected,
|
||||
@@ -1215,8 +1217,9 @@ for (const channel of [
|
||||
|
||||
app.whenReady().then(() => {
|
||||
const window = new BrowserWindow({
|
||||
width: 1440,
|
||||
height: 960,
|
||||
// Keep the E2E window aligned with createWindow() in src/main/index.ts.
|
||||
width: 1400,
|
||||
height: 800,
|
||||
show: false,
|
||||
backgroundColor: '#ffffff',
|
||||
webPreferences: {
|
||||
@@ -1226,7 +1229,7 @@ app.whenReady().then(() => {
|
||||
sandbox: false
|
||||
}
|
||||
})
|
||||
window.once('ready-to-show', () => window.show())
|
||||
if (!backgroundE2E) window.once('ready-to-show', () => window.show())
|
||||
window.loadFile(path.join(root, 'out/renderer/index.html'))
|
||||
})
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ export interface TestApplication {
|
||||
app: ElectronApplication
|
||||
page: Page
|
||||
userData: string
|
||||
setWindowContentSize: (size: { width: number; height: number }) => Promise<void>
|
||||
close: () => Promise<void>
|
||||
}
|
||||
|
||||
@@ -60,28 +59,10 @@ export async function launchTestApp(
|
||||
const page = await app.firstWindow()
|
||||
await page.waitForLoadState('domcontentloaded')
|
||||
if (options.now) await page.clock.setFixedTime(options.now)
|
||||
const setWindowContentSize = async (size: { width: number; height: number }): Promise<void> => {
|
||||
await app.evaluate(({ BrowserWindow, screen }, nextSize) => {
|
||||
const [window] = BrowserWindow.getAllWindows()
|
||||
if (!window) throw new Error('E2E BrowserWindow is unavailable')
|
||||
const { workArea } = screen.getPrimaryDisplay()
|
||||
window.setPosition(workArea.x + 40, workArea.y + 40)
|
||||
window.setContentSize(nextSize.width, nextSize.height)
|
||||
}, size)
|
||||
await page.waitForFunction(
|
||||
(nextSize) => window.innerWidth === nextSize.width && window.innerHeight === nextSize.height,
|
||||
size
|
||||
)
|
||||
await page.evaluate(
|
||||
() =>
|
||||
new Promise<void>((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))
|
||||
)
|
||||
}
|
||||
return {
|
||||
app,
|
||||
page,
|
||||
userData,
|
||||
setWindowContentSize,
|
||||
close: async () => {
|
||||
if (!page.isClosed() && closeDelayMs > 0) await page.waitForTimeout(closeDelayMs)
|
||||
await app.close().catch(() => undefined)
|
||||
|
||||
@@ -4,7 +4,6 @@ import { resolve } from 'path'
|
||||
import { launchTestApp } from './support/electron'
|
||||
|
||||
const baselineDirectory = resolve(`tests/e2e/__screenshots__/${process.platform}/visual.spec.ts`)
|
||||
const visualViewport = { width: 1400, height: 772 }
|
||||
const visualNow = Date.parse('2026-08-19T14:46:40+08:00')
|
||||
|
||||
async function clearScreenshotFocus(page: import('@playwright/test').Page): Promise<void> {
|
||||
@@ -21,7 +20,6 @@ test.skip(
|
||||
test('NAV-01 login page visual @visual', async () => {
|
||||
const fixture = await launchTestApp({ mode: 'disconnected' })
|
||||
try {
|
||||
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', {
|
||||
@@ -36,7 +34,6 @@ test('NAV-01 login page visual @visual', async () => {
|
||||
test('ARCH-01 archive page visual @visual', async () => {
|
||||
const fixture = await launchTestApp({ now: visualNow })
|
||||
try {
|
||||
await fixture.setWindowContentSize(visualViewport)
|
||||
await fixture.page.getByText('产品测试群', { exact: true }).click()
|
||||
await expect(fixture.page.getByText('这是一条脱敏测试消息', { exact: true })).toBeVisible()
|
||||
await clearScreenshotFocus(fixture.page)
|
||||
@@ -54,7 +51,6 @@ test('CHAT-01 archive search controls visual @visual', async () => {
|
||||
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()
|
||||
const searchInput = fixture.page.getByRole('textbox', { name: '搜索当前聊天内容' })
|
||||
@@ -81,7 +77,6 @@ test('ASK-01 AI Search idle page visual @visual', async () => {
|
||||
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()
|
||||
@@ -105,7 +100,6 @@ test('ASK-03 AI Search result page visual @visual', async () => {
|
||||
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()
|
||||
@@ -132,7 +126,6 @@ test('ASK-04 AI Search idle page dark visual @visual', async () => {
|
||||
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')
|
||||
@@ -155,7 +148,6 @@ test('ASK-05 AI Search result page dark visual @visual', async () => {
|
||||
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()
|
||||
@@ -182,7 +174,6 @@ test('API-00 Reader Skill page visual @visual', async () => {
|
||||
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 expect(fixture.page.getByRole('heading', { name: 'TraceMemo Reader' })).toBeVisible()
|
||||
await expect(fixture.page.getByText('API Token', { exact: true })).toBeVisible()
|
||||
@@ -205,7 +196,6 @@ test('API-00 Reader Skill page dark visual @visual', async () => {
|
||||
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 expect(fixture.page.getByRole('heading', { name: 'TraceMemo Reader' })).toBeVisible()
|
||||
await expect(fixture.page.getByText('API Token', { exact: true })).toBeVisible()
|
||||
@@ -229,7 +219,6 @@ test('API-01 Reader Skill preview visual @visual', async () => {
|
||||
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')
|
||||
@@ -257,7 +246,6 @@ test('API-02 Agent target segmented control visual @visual', async () => {
|
||||
const pageErrors: Error[] = []
|
||||
fixture.page.on('pageerror', (error) => pageErrors.push(error))
|
||||
try {
|
||||
await fixture.setWindowContentSize(visualViewport)
|
||||
await fixture.page.getByRole('button', { name: 'API' }).click()
|
||||
const targetGroup = fixture.page.getByRole('radiogroup', { name: '选择目标 Agent' })
|
||||
await targetGroup.evaluate((element) => element.scrollIntoView({ block: 'center' }))
|
||||
@@ -289,7 +277,6 @@ for (const appearanceTheme of ['light', 'dark'] as const) {
|
||||
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: '退群监控', exact: true })
|
||||
@@ -317,7 +304,6 @@ for (const appearanceTheme of ['light', 'dark'] as const) {
|
||||
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.getByRole('button', { name: '管理群聊' }).click()
|
||||
await expect(
|
||||
@@ -346,7 +332,6 @@ for (const appearanceTheme of ['light', 'dark'] as const) {
|
||||
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.getByRole('button', { name: '管理群聊' }).click()
|
||||
await fixture.page.getByRole('checkbox', { name: '监控产品测试群' }).click()
|
||||
@@ -375,7 +360,6 @@ for (const appearanceTheme of ['light', 'dark'] as const) {
|
||||
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.getByRole('button', { name: '开始生成日报' }).click()
|
||||
await fixture.page.locator('.report-source-item').filter({ hasText: '产品测试群' }).click()
|
||||
@@ -436,7 +420,6 @@ for (const appearanceTheme of ['light', 'dark'] as const) {
|
||||
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.getByRole('button', { name: '开始生成日报' }).click()
|
||||
await fixture.page.locator('.report-source-item').filter({ hasText: '产品测试群' }).click()
|
||||
@@ -470,7 +453,6 @@ for (const appearanceTheme of ['light', 'dark'] as const) {
|
||||
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()
|
||||
const dialog = fixture.page.getByRole('dialog', { name: '产品测试群' })
|
||||
@@ -516,7 +498,6 @@ test('CHAT-03 image viewer visual @visual', async () => {
|
||||
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()
|
||||
@@ -539,7 +520,6 @@ test('SETTINGS-02 basic settings controls visual @visual', async () => {
|
||||
const pageErrors: Error[] = []
|
||||
fixture.page.on('pageerror', (error) => pageErrors.push(error))
|
||||
try {
|
||||
await fixture.setWindowContentSize(visualViewport)
|
||||
await fixture.page
|
||||
.getByRole('navigation', { name: '一级导航' })
|
||||
.getByRole('button', { name: '设置' })
|
||||
@@ -590,7 +570,6 @@ test('UPDATE-01 startup prompt and downloaded state visual @visual', async () =>
|
||||
const pageErrors: Error[] = []
|
||||
fixture.page.on('pageerror', (error) => pageErrors.push(error))
|
||||
try {
|
||||
await fixture.setWindowContentSize(visualViewport)
|
||||
const dialog = fixture.page.getByRole('alertdialog', { name: '发现新版本 v2.0.0' })
|
||||
await expect(dialog).toBeVisible()
|
||||
await clearScreenshotFocus(fixture.page)
|
||||
@@ -620,7 +599,6 @@ test('UPDATE-02 unsigned macOS release prompt visual @visual', async () => {
|
||||
const pageErrors: Error[] = []
|
||||
fixture.page.on('pageerror', (error) => pageErrors.push(error))
|
||||
try {
|
||||
await fixture.setWindowContentSize(visualViewport)
|
||||
const dialog = fixture.page.getByRole('alertdialog', { name: '发现新版本 v2.2.3' })
|
||||
await expect(dialog).toBeVisible()
|
||||
await clearScreenshotFocus(fixture.page)
|
||||
@@ -656,7 +634,6 @@ test('SETTINGS-03 database key controls visual @visual', async () => {
|
||||
const pageErrors: Error[] = []
|
||||
fixture.page.on('pageerror', (error) => pageErrors.push(error))
|
||||
try {
|
||||
await fixture.setWindowContentSize(visualViewport)
|
||||
await fixture.page
|
||||
.getByRole('navigation', { name: '一级导航' })
|
||||
.getByRole('button', { name: '设置' })
|
||||
@@ -690,7 +667,6 @@ for (const appearanceTheme of ['light', 'dark'] as const) {
|
||||
const pageErrors: Error[] = []
|
||||
fixture.page.on('pageerror', (error) => pageErrors.push(error))
|
||||
try {
|
||||
await fixture.setWindowContentSize(visualViewport)
|
||||
await fixture.page
|
||||
.getByRole('navigation', { name: '一级导航' })
|
||||
.getByRole('button', { name: '设置' })
|
||||
@@ -722,7 +698,6 @@ for (const appearanceTheme of ['light', 'dark'] as const) {
|
||||
const pageErrors: Error[] = []
|
||||
fixture.page.on('pageerror', (error) => pageErrors.push(error))
|
||||
try {
|
||||
await fixture.setWindowContentSize(visualViewport)
|
||||
await fixture.page
|
||||
.getByRole('navigation', { name: '一级导航' })
|
||||
.getByRole('button', { name: '设置' })
|
||||
@@ -758,7 +733,6 @@ for (const appearanceTheme of ['light', 'dark'] as const) {
|
||||
const pageErrors: Error[] = []
|
||||
fixture.page.on('pageerror', (error) => pageErrors.push(error))
|
||||
try {
|
||||
await fixture.setWindowContentSize(visualViewport)
|
||||
await fixture.page
|
||||
.getByRole('navigation', { name: '一级导航' })
|
||||
.getByRole('button', { name: '设置' })
|
||||
@@ -796,7 +770,6 @@ for (const appearanceTheme of ['light', 'dark'] as const) {
|
||||
const pageErrors: Error[] = []
|
||||
fixture.page.on('pageerror', (error) => pageErrors.push(error))
|
||||
try {
|
||||
await fixture.setWindowContentSize(visualViewport)
|
||||
await fixture.page
|
||||
.getByRole('navigation', { name: '一级导航' })
|
||||
.getByRole('button', { name: '设置' })
|
||||
@@ -827,11 +800,11 @@ for (const appearanceTheme of ['light', 'dark'] as const) {
|
||||
|
||||
for (const appearanceTheme of ['light', 'dark'] as const) {
|
||||
test(`SETTINGS-06A voice encoding environment ${appearanceTheme} visual @visual`, async () => {
|
||||
test.skip(process.platform !== 'darwin', 'Voice encoding environment is macOS-only')
|
||||
const fixture = await launchTestApp({ now: visualNow, appearanceTheme })
|
||||
const pageErrors: Error[] = []
|
||||
fixture.page.on('pageerror', (error) => pageErrors.push(error))
|
||||
try {
|
||||
await fixture.setWindowContentSize(visualViewport)
|
||||
await fixture.page
|
||||
.getByRole('navigation', { name: '一级导航' })
|
||||
.getByRole('button', { name: '设置' })
|
||||
@@ -867,7 +840,6 @@ for (const appearanceTheme of ['light', 'dark'] as const) {
|
||||
const pageErrors: Error[] = []
|
||||
fixture.page.on('pageerror', (error) => pageErrors.push(error))
|
||||
try {
|
||||
await fixture.setWindowContentSize(visualViewport)
|
||||
await fixture.page
|
||||
.getByRole('navigation', { name: '一级导航' })
|
||||
.getByRole('button', { name: '设置' })
|
||||
@@ -902,7 +874,6 @@ for (const appearanceTheme of ['light', 'dark'] as const) {
|
||||
const pageErrors: Error[] = []
|
||||
fixture.page.on('pageerror', (error) => pageErrors.push(error))
|
||||
try {
|
||||
await fixture.setWindowContentSize(visualViewport)
|
||||
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()
|
||||
@@ -927,7 +898,6 @@ test('EXPORT-01 export workspace idle visual @visual', async () => {
|
||||
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()
|
||||
@@ -950,7 +920,6 @@ test('THEME-01 archive page dark visual @visual', async () => {
|
||||
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')
|
||||
@@ -973,7 +942,6 @@ test('THEME-02 export workspace dark visual @visual', async () => {
|
||||
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')
|
||||
|
||||
@@ -1047,7 +1047,7 @@ describe('media export flow', () => {
|
||||
dirname(first.outputPath!),
|
||||
firstArchive.messages[0].exportMediaUrl as string
|
||||
)
|
||||
expect(statSync(videoPath).mode & 0o777).toBe(0o644)
|
||||
expect(statSync(videoPath).mode & 0o200).toBe(0o200)
|
||||
|
||||
chmodSync(videoPath, 0o444)
|
||||
const second = await runExport(
|
||||
@@ -1057,7 +1057,7 @@ describe('media export flow', () => {
|
||||
|
||||
expect(second.success, second.error).toBe(true)
|
||||
expect(second.outputPath).toBe(first.outputPath)
|
||||
expect(statSync(videoPath).mode & 0o777).toBe(0o644)
|
||||
expect(statSync(videoPath).mode & 0o200).toBe(0o200)
|
||||
})
|
||||
|
||||
it('merges two conversations in stable order without colliding identical message ids or media', async () => {
|
||||
|
||||
@@ -33,7 +33,9 @@ describe('ApiTokenStore', () => {
|
||||
const first = firstStore.revealToken().token
|
||||
expect(first).toMatch(/^[A-Za-z0-9_-]{43}$/)
|
||||
expect(fs.readFileSync(filePath, 'utf8')).not.toContain(String(first))
|
||||
if (process.platform !== 'win32') {
|
||||
expect(fs.statSync(filePath).mode & 0o777).toBe(0o600)
|
||||
}
|
||||
|
||||
const secondStore = new ApiTokenStore(filePath)
|
||||
expect(secondStore.ensureToken()).toMatchObject({ success: true, hasToken: true })
|
||||
|
||||
@@ -224,7 +224,7 @@ describe('TraceMemo app data migration', () => {
|
||||
const current = new SqliteTranscriptRepository(currentPath)
|
||||
expect(current.findLatest('account-a', 'message-a')?.transcript).toBe('已经转写过的文字')
|
||||
current.close()
|
||||
})
|
||||
}, 15_000)
|
||||
|
||||
it('backfills voice transcripts after the original migration was already completed', async () => {
|
||||
const fixture = roots()
|
||||
@@ -264,7 +264,7 @@ describe('TraceMemo app data migration', () => {
|
||||
)
|
||||
expect(current.findLatest('account-a', 'message-a')?.transcript).toBe('补迁文字')
|
||||
current.close()
|
||||
})
|
||||
}, 15_000)
|
||||
|
||||
it('does not retry a voice transcript backfill after it was marked failed', async () => {
|
||||
const fixture = roots()
|
||||
|
||||
@@ -99,8 +99,13 @@ describe('scheduled report scheduling', () => {
|
||||
expect(validateScheduleTime('09:05')).toBe(true)
|
||||
expect(validateScheduleTime('24:00')).toBe(false)
|
||||
const from = new Date('2026-08-27T10:00:00+08:00')
|
||||
expect(calculateNextRunAt('09:05', from)).toBe('2026-08-28T01:05:00.000Z')
|
||||
expect(calculateNextRunAt('11:05', from)).toBe('2026-08-27T03:05:00.000Z')
|
||||
const expectedNext = new Date(from)
|
||||
expectedNext.setHours(9, 5, 0, 0)
|
||||
if (expectedNext.getTime() <= from.getTime()) expectedNext.setDate(expectedNext.getDate() + 1)
|
||||
expect(calculateNextRunAt('09:05', from)).toBe(expectedNext.toISOString())
|
||||
const expectedSameDay = new Date(from)
|
||||
expectedSameDay.setHours(11, 5, 0, 0)
|
||||
expect(calculateNextRunAt('11:05', from)).toBe(expectedSameDay.toISOString())
|
||||
})
|
||||
|
||||
it('defaults notification settings to off and persists a successful enablement', async () => {
|
||||
|
||||
@@ -19,6 +19,7 @@ describe('StickerService', () => {
|
||||
beforeEach(() => {
|
||||
homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sticker-service-'))
|
||||
vi.stubEnv('HOME', homeDir)
|
||||
vi.stubEnv('USERPROFILE', homeDir)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
Reference in New Issue
Block a user