diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 440b60f..ef8d97a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 diff --git a/docs/user-guide/monitor-automation.md b/docs/user-guide/monitor-automation.md deleted file mode 100644 index 9b720e6..0000000 --- a/docs/user-guide/monitor-automation.md +++ /dev/null @@ -1,2 +0,0 @@ -# monitor-automation - diff --git a/package.json b/package.json index 11705e2..562badd 100644 --- a/package.json +++ b/package.json @@ -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": [ diff --git a/services/wechat-connector/ilink/auth_test.go b/services/wechat-connector/ilink/auth_test.go index 913ff8f..6c25a0c 100644 --- a/services/wechat-connector/ilink/auth_test.go +++ b/services/wechat-connector/ilink/auth_test.go @@ -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) diff --git a/src/renderer/src/components/chat/PersonalWechatSendDialog.tsx b/src/renderer/src/components/chat/PersonalWechatSendDialog.tsx index 79c6173..5235ab3 100644 --- a/src/renderer/src/components/chat/PersonalWechatSendDialog.tsx +++ b/src/renderer/src/components/chat/PersonalWechatSendDialog.tsx @@ -308,6 +308,8 @@ function PersonalWechatMacSendDialog({ <> !open && handleClose()}> { restoreFocusRef.current = diff --git a/src/renderer/src/components/chat/PersonalWechatWindowsSendDialog.tsx b/src/renderer/src/components/chat/PersonalWechatWindowsSendDialog.tsx index d0c48ab..ac2f7ba 100644 --- a/src/renderer/src/components/chat/PersonalWechatWindowsSendDialog.tsx +++ b/src/renderer/src/components/chat/PersonalWechatWindowsSendDialog.tsx @@ -170,6 +170,8 @@ export function PersonalWechatWindowsSendDialog({ return ( !open && handleClose()}> { restoreFocusRef.current = diff --git a/src/renderer/src/components/reports/ReportViewer.tsx b/src/renderer/src/components/reports/ReportViewer.tsx index f00febd..2235f86 100644 --- a/src/renderer/src/components/reports/ReportViewer.tsx +++ b/src/renderer/src/components/reports/ReportViewer.tsx @@ -65,12 +65,12 @@ export function ReportViewer({ const viewportRef = useRef(null) useEffect(() => { + setSendDialogOpen(false) const frame = window.requestAnimationFrame(() => { setStatus('') setImageError('') setIsSwitchingTemplate(false) setShareDialogOpen(false) - setSendDialogOpen(false) setZoom(1) setFitZoom(1) setNaturalSize(null) diff --git a/src/renderer/src/components/reports/ScheduledReportsWorkspace.tsx b/src/renderer/src/components/reports/ScheduledReportsWorkspace.tsx index c82ae25..3e1e9ef 100644 --- a/src/renderer/src/components/reports/ScheduledReportsWorkspace.tsx +++ b/src/renderer/src/components/reports/ScheduledReportsWorkspace.tsx @@ -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 diff --git a/src/renderer/src/styles/foundation.scss b/src/renderer/src/styles/foundation.scss index 2766648..5e9f42a 100644 --- a/src/renderer/src/styles/foundation.scss +++ b/src/renderer/src/styles/foundation.scss @@ -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; } @@ -368,4 +378,4 @@ body { min-width: 0; align-items: stretch; overflow: hidden; -} \ No newline at end of file +} diff --git a/tests/component/report-features.test.tsx b/tests/component/report-features.test.tsx index 76a7020..31ce7df 100644 --- a/tests/component/report-features.test.tsx +++ b/tests/component/report-features.test.tsx @@ -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', () => { diff --git a/tests/e2e/__screenshots__/win32/visual.spec.ts/archive-page.png b/tests/e2e/__screenshots__/win32/visual.spec.ts/archive-page.png index b1ad22f..88876f5 100644 Binary files a/tests/e2e/__screenshots__/win32/visual.spec.ts/archive-page.png and b/tests/e2e/__screenshots__/win32/visual.spec.ts/archive-page.png differ diff --git a/tests/e2e/__screenshots__/win32/visual.spec.ts/login-page.png b/tests/e2e/__screenshots__/win32/visual.spec.ts/login-page.png index f49bf2c..170b387 100644 Binary files a/tests/e2e/__screenshots__/win32/visual.spec.ts/login-page.png and b/tests/e2e/__screenshots__/win32/visual.spec.ts/login-page.png differ diff --git a/tests/e2e/app.spec.ts b/tests/e2e/app.spec.ts index b5a3b0f..c070bc4 100644 --- a/tests/e2e/app.spec.ts +++ b/tests/e2e/app.spec.ts @@ -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) diff --git a/tests/e2e/export-archive.spec.ts b/tests/e2e/export-archive.spec.ts index b9ee47a..9ded770 100644 --- a/tests/e2e/export-archive.spec.ts +++ b/tests/e2e/export-archive.spec.ts @@ -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 }) diff --git a/tests/e2e/scheduled-reports.spec.ts b/tests/e2e/scheduled-reports.spec.ts index 1bad708..829a52f 100644 --- a/tests/e2e/scheduled-reports.spec.ts +++ b/tests/e2e/scheduled-reports.spec.ts @@ -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() diff --git a/tests/e2e/support/electron-main.cjs b/tests/e2e/support/electron-main.cjs index 1eff916..913616f 100644 --- a/tests/e2e/support/electron-main.cjs +++ b/tests/e2e/support/electron-main.cjs @@ -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')) }) diff --git a/tests/e2e/support/electron.ts b/tests/e2e/support/electron.ts index f8657b4..4e1ebd5 100644 --- a/tests/e2e/support/electron.ts +++ b/tests/e2e/support/electron.ts @@ -10,7 +10,6 @@ export interface TestApplication { app: ElectronApplication page: Page userData: string - setWindowContentSize: (size: { width: number; height: number }) => Promise close: () => Promise } @@ -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 => { - 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((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) diff --git a/tests/e2e/visual.spec.ts b/tests/e2e/visual.spec.ts index cb90bd1..c8374e9 100644 --- a/tests/e2e/visual.spec.ts +++ b/tests/e2e/visual.spec.ts @@ -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 { @@ -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') diff --git a/tests/integration/export-media-flow.test.ts b/tests/integration/export-media-flow.test.ts index b36cc1a..f3d5972 100644 --- a/tests/integration/export-media-flow.test.ts +++ b/tests/integration/export-media-flow.test.ts @@ -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 () => { diff --git a/tests/unit/api-token-store.test.ts b/tests/unit/api-token-store.test.ts index fdcf957..6e26fac 100644 --- a/tests/unit/api-token-store.test.ts +++ b/tests/unit/api-token-store.test.ts @@ -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)) - expect(fs.statSync(filePath).mode & 0o777).toBe(0o600) + 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 }) diff --git a/tests/unit/app-data-migration.test.ts b/tests/unit/app-data-migration.test.ts index c4a69ce..bf1dfdf 100644 --- a/tests/unit/app-data-migration.test.ts +++ b/tests/unit/app-data-migration.test.ts @@ -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() diff --git a/tests/unit/scheduled-report-service.test.ts b/tests/unit/scheduled-report-service.test.ts index 613ac9b..302a4b4 100644 --- a/tests/unit/scheduled-report-service.test.ts +++ b/tests/unit/scheduled-report-service.test.ts @@ -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 () => { diff --git a/tests/unit/sticker-service.test.ts b/tests/unit/sticker-service.test.ts index c32b358..c0dd113 100644 --- a/tests/unit/sticker-service.test.ts +++ b/tests/unit/sticker-service.test.ts @@ -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 () => {