mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-10-03 18:33:14 +08:00
feat: 增加windows发送能力
抽离功能到设置页面 抽离文字转语音功能
This commit is contained in:
@@ -4,6 +4,13 @@ import type React from 'react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PersonalWechatSendDialog } from '../../src/renderer/src/components/chat/PersonalWechatSendDialog'
|
||||
|
||||
vi.mock('../../src/renderer/src/utils/runtime-environment', () => ({
|
||||
isMac: true,
|
||||
isWindows: false,
|
||||
runtimePlatform: 'darwin',
|
||||
supportsPersonalWechatSend: true
|
||||
}))
|
||||
|
||||
const getStatus = vi.fn()
|
||||
const getRuntimeStatus = vi.fn()
|
||||
const downloadRuntime = vi.fn()
|
||||
@@ -73,9 +80,12 @@ function renderDialog(
|
||||
}
|
||||
|
||||
async function startComposer(): Promise<void> {
|
||||
const bind = await screen.findByRole('button', { name: '绑定微信' })
|
||||
fireEvent.click(bind)
|
||||
await screen.findByText('✓ 微信已绑定')
|
||||
await screen.findByText('绑定个人微信')
|
||||
const bind = screen.queryByRole('button', { name: '绑定微信' })
|
||||
if (bind) {
|
||||
fireEvent.click(bind)
|
||||
await screen.findByText('✓ 微信已绑定')
|
||||
}
|
||||
const detect = await screen.findByRole('button', { name: '重新检测' })
|
||||
fireEvent.click(detect)
|
||||
const start = await screen.findByRole('button', { name: '开始发送' })
|
||||
@@ -170,8 +180,8 @@ describe('PersonalWechatSendDialog', () => {
|
||||
message: '尚未绑定当前微信'
|
||||
})
|
||||
renderDialog()
|
||||
expect(await screen.findByText('准备语音模型')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: '下载模型' })).toBeEnabled()
|
||||
expect(await screen.findByText('准备 OneBot 运行时')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: '下载运行时' })).toBeEnabled()
|
||||
expect(screen.getByText('绑定个人微信')).toBeInTheDocument()
|
||||
expect(screen.getByText('验证消息能力')).toBeInTheDocument()
|
||||
expect(screen.getByText('能力检测')).toBeInTheDocument()
|
||||
@@ -250,7 +260,7 @@ describe('PersonalWechatSendDialog', () => {
|
||||
})
|
||||
fireEvent.click(screen.getByRole('button', { name: '生成语音' }))
|
||||
await waitFor(() => expect(screen.getByText('语音已生成')).toBeInTheDocument())
|
||||
fireEvent.click(screen.getByRole('button', { name: '发送2' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: '空白语音?重新处理' }))
|
||||
await waitFor(() =>
|
||||
expect(sendMessage).toHaveBeenCalledWith({
|
||||
type: 'voice',
|
||||
@@ -286,8 +296,8 @@ describe('PersonalWechatSendDialog', () => {
|
||||
canSendVoice: false
|
||||
})
|
||||
renderDialog()
|
||||
await screen.findByRole('button', { name: '下载模型' })
|
||||
fireEvent.click(screen.getByRole('button', { name: '下载模型' }))
|
||||
await screen.findByRole('button', { name: '下载运行时' })
|
||||
fireEvent.click(screen.getByRole('button', { name: '下载运行时' }))
|
||||
await waitFor(() => expect(downloadRuntime).toHaveBeenCalledOnce())
|
||||
fireEvent.click(screen.getByRole('button', { name: '绑定微信' }))
|
||||
await waitFor(() => expect(rebind).toHaveBeenCalledOnce())
|
||||
@@ -320,8 +330,11 @@ describe('PersonalWechatSendDialog', () => {
|
||||
})
|
||||
renderDialog()
|
||||
expect(await screen.findByText('图片和语音消息')).toBeInTheDocument()
|
||||
fireEvent.click(screen.getByRole('button', { name: '绑定微信' }))
|
||||
await screen.findByText('✓ 微信已绑定')
|
||||
const bind = screen.queryByRole('button', { name: '绑定微信' })
|
||||
if (bind) {
|
||||
fireEvent.click(bind)
|
||||
await screen.findByText('✓ 微信已绑定')
|
||||
}
|
||||
fireEvent.click(await screen.findByRole('button', { name: '重新检测' }))
|
||||
expect(screen.getByText('微信消息发送已配置完成')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: '开始发送' })).toBeEnabled()
|
||||
@@ -340,8 +353,12 @@ describe('PersonalWechatSendDialog', () => {
|
||||
message: '等待消息初始化'
|
||||
})
|
||||
renderDialog()
|
||||
fireEvent.click(await screen.findByRole('button', { name: '绑定微信' }))
|
||||
await screen.findByText('✓ 微信已绑定')
|
||||
await screen.findByText('绑定个人微信')
|
||||
const bind = screen.queryByRole('button', { name: '绑定微信' })
|
||||
if (bind) {
|
||||
fireEvent.click(bind)
|
||||
await screen.findByText('✓ 微信已绑定')
|
||||
}
|
||||
const detect = await screen.findByRole('button', { name: '重新检测' })
|
||||
expect(detect).toBeEnabled()
|
||||
fireEvent.click(detect)
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PersonalWechatSendPage } from '../../src/renderer/src/features/settings/pages/PersonalWechatSendPage'
|
||||
|
||||
vi.mock('../../src/renderer/src/utils/runtime-environment', () => ({
|
||||
isMac: true,
|
||||
isWindows: false,
|
||||
runtimePlatform: 'darwin',
|
||||
supportsPersonalWechatSend: true
|
||||
}))
|
||||
|
||||
const getCapability = vi.fn()
|
||||
const getSenderStatus = vi.fn()
|
||||
const getRuntimeStatus = vi.fn()
|
||||
const onRuntimeProgress = vi.fn(() => vi.fn())
|
||||
const downloadRuntime = vi.fn()
|
||||
const cancelRuntimeDownload = vi.fn()
|
||||
const removeRuntime = vi.fn()
|
||||
const openRuntimeDirectory = vi.fn()
|
||||
const rebindSender = vi.fn()
|
||||
|
||||
const senderStatus = {
|
||||
state: 'online' as const,
|
||||
platform: 'darwin',
|
||||
arch: 'arm64',
|
||||
sipDisabled: true,
|
||||
wechatRunning: true,
|
||||
wechatPid: 4668,
|
||||
boundWechatPid: 4668,
|
||||
oneBotPid: 5401,
|
||||
endpoint: '127.0.0.1:58080',
|
||||
endpointReady: true,
|
||||
wechatVersion: '4.1.11.53',
|
||||
runtimeReady: true,
|
||||
attachReady: true,
|
||||
baseAddress: '0x114ef8000',
|
||||
baseAddressReady: true,
|
||||
textHookInstalled: true,
|
||||
textHookReady: true,
|
||||
imageHookInstalled: true,
|
||||
imageHookReady: true,
|
||||
messageListenerReady: true,
|
||||
canSend: true,
|
||||
canSendText: true,
|
||||
canSendImage: true,
|
||||
canSendVoice: true,
|
||||
message: '个人微信已绑定'
|
||||
}
|
||||
|
||||
const readyRuntime = {
|
||||
version: 'v0.0.18',
|
||||
state: 'ready' as const,
|
||||
downloadedBytes: 66_599_785,
|
||||
totalBytes: 66_599_785,
|
||||
progress: 1,
|
||||
platform: 'darwin' as NodeJS.Platform,
|
||||
architecture: 'arm64',
|
||||
supported: true,
|
||||
removable: true,
|
||||
directory: '/tmp/personal-wechat-runtime'
|
||||
}
|
||||
|
||||
const capability = {
|
||||
supported: true,
|
||||
ready: true,
|
||||
status: 'ready' as const,
|
||||
capabilities: { text: true, image: true, voice: true },
|
||||
senderStatus,
|
||||
message: '个人微信发送能力已就绪'
|
||||
}
|
||||
|
||||
describe('PersonalWechatSendPage on macOS', () => {
|
||||
beforeEach(() => {
|
||||
getCapability.mockReset().mockResolvedValue(capability)
|
||||
getSenderStatus.mockReset().mockResolvedValue(senderStatus)
|
||||
getRuntimeStatus.mockReset().mockResolvedValue(readyRuntime)
|
||||
onRuntimeProgress.mockReset().mockReturnValue(vi.fn())
|
||||
downloadRuntime.mockReset().mockResolvedValue({ success: true, status: readyRuntime })
|
||||
cancelRuntimeDownload.mockReset().mockResolvedValue({ success: true })
|
||||
removeRuntime.mockReset().mockResolvedValue({
|
||||
...readyRuntime,
|
||||
state: 'missing',
|
||||
downloadedBytes: 0,
|
||||
progress: 0,
|
||||
removable: false,
|
||||
directory: undefined
|
||||
})
|
||||
openRuntimeDirectory.mockReset().mockResolvedValue({ success: true })
|
||||
rebindSender.mockReset().mockResolvedValue(senderStatus)
|
||||
vi.spyOn(window, 'confirm').mockReturnValue(true)
|
||||
Object.defineProperty(window, 'api', {
|
||||
configurable: true,
|
||||
value: {
|
||||
getPersonalWechatSendCapability: getCapability,
|
||||
getPersonalWechatSenderStatus: getSenderStatus,
|
||||
getPersonalWechatRuntimeStatus: getRuntimeStatus,
|
||||
onPersonalWechatRuntimeProgress: onRuntimeProgress,
|
||||
downloadPersonalWechatRuntime: downloadRuntime,
|
||||
cancelPersonalWechatRuntimeDownload: cancelRuntimeDownload,
|
||||
removePersonalWechatRuntime: removeRuntime,
|
||||
openPersonalWechatRuntimeDirectory: openRuntimeDirectory,
|
||||
rebindPersonalWechatSender: rebindSender
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps OneBot runtime management on the WeChat send page', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<PersonalWechatSendPage onNotice={vi.fn()} />)
|
||||
|
||||
expect(await screen.findByText('OneBot v0.0.18')).toBeVisible()
|
||||
expect(screen.getAllByText('已就绪')).not.toHaveLength(0)
|
||||
await user.click(screen.getByRole('button', { name: '打开目录' }))
|
||||
expect(openRuntimeDirectory).toHaveBeenCalledOnce()
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '卸载组件' }))
|
||||
await waitFor(() => expect(removeRuntime).toHaveBeenCalledOnce())
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '支持版本' }))
|
||||
const versionsDialog = screen.getByRole('dialog', { name: '支持的微信版本' })
|
||||
expect(versionsDialog).toBeVisible()
|
||||
expect(versionsDialog).toHaveTextContent('4.1.11.53')
|
||||
})
|
||||
|
||||
it('downloads a missing macOS runtime from the send page', async () => {
|
||||
getRuntimeStatus.mockResolvedValue({
|
||||
...readyRuntime,
|
||||
state: 'missing',
|
||||
downloadedBytes: 0,
|
||||
progress: 0,
|
||||
removable: false,
|
||||
directory: undefined
|
||||
})
|
||||
const onNotice = vi.fn()
|
||||
render(<PersonalWechatSendPage onNotice={onNotice} />)
|
||||
|
||||
await userEvent.setup().click(await screen.findByRole('button', { name: '下载组件' }))
|
||||
await waitFor(() => expect(downloadRuntime).toHaveBeenCalledOnce())
|
||||
expect(onNotice).toHaveBeenCalledWith('微信发送组件已准备好')
|
||||
})
|
||||
})
|
||||
@@ -609,6 +609,8 @@ describe('daily report controls', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: '发送到当前群聊' }))
|
||||
|
||||
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()
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PersonalWechatSendDialog } from '../../src/renderer/src/components/chat/PersonalWechatSendDialog'
|
||||
import { PersonalWechatSendPage } from '../../src/renderer/src/features/settings/pages/PersonalWechatSendPage'
|
||||
import { TextToSpeechPage } from '../../src/renderer/src/features/settings/pages/TextToSpeechPage'
|
||||
|
||||
vi.mock('../../src/renderer/src/utils/runtime-environment', () => ({
|
||||
isMac: false,
|
||||
isWindows: true,
|
||||
runtimePlatform: 'win32',
|
||||
supportsPersonalWechatSend: true
|
||||
}))
|
||||
|
||||
const getTextToSpeechSettings = vi.fn()
|
||||
const listTextToSpeechVoices = vi.fn()
|
||||
const openFishAudioApiKeys = vi.fn()
|
||||
const getSettings = vi.fn()
|
||||
const setSettings = vi.fn()
|
||||
const getPersonalWechatSenderStatus = vi.fn()
|
||||
const getPersonalWechatRuntimeStatus = vi.fn()
|
||||
const checkPersonalWechatSenderStatus = vi.fn()
|
||||
|
||||
const textToSpeechSettings = {
|
||||
success: true,
|
||||
settings: {
|
||||
provider: 'fish-audio' as const,
|
||||
hasApiKey: false,
|
||||
hasStoredApiKey: false,
|
||||
hasEnvironmentApiKey: false,
|
||||
keySource: 'none' as const,
|
||||
encryptionAvailable: true,
|
||||
selectedVoiceId: '',
|
||||
outputFormat: 'mp3' as const,
|
||||
model: 's2.1-pro-free' as const,
|
||||
phase: 'idle' as const
|
||||
},
|
||||
voices: []
|
||||
}
|
||||
|
||||
const windowsStatus = {
|
||||
state: 'online' as const,
|
||||
platform: 'win32',
|
||||
arch: 'x64',
|
||||
sipDisabled: true,
|
||||
wechatRunning: true,
|
||||
runtimeReady: true,
|
||||
endpoint: '127.0.0.1:4567',
|
||||
endpointReady: true,
|
||||
attachReady: true,
|
||||
baseAddressReady: true,
|
||||
textHookInstalled: false,
|
||||
textHookReady: false,
|
||||
imageHookInstalled: false,
|
||||
imageHookReady: false,
|
||||
messageListenerReady: false,
|
||||
canSend: true,
|
||||
canSendText: true,
|
||||
canSendImage: true,
|
||||
canSendVoice: true,
|
||||
message: 'Windows 微信发送能力已连接,可以发送消息'
|
||||
}
|
||||
|
||||
describe('PersonalWechatSendPage on Windows', () => {
|
||||
beforeEach(() => {
|
||||
getTextToSpeechSettings.mockReset().mockResolvedValue(textToSpeechSettings)
|
||||
listTextToSpeechVoices.mockReset()
|
||||
openFishAudioApiKeys.mockReset().mockResolvedValue({ success: true })
|
||||
getSettings.mockReset().mockResolvedValue({ settings: { windowsWechatPort: '' } })
|
||||
setSettings.mockReset().mockResolvedValue({
|
||||
settings: { windowsWechatPort: '4567' }
|
||||
})
|
||||
getPersonalWechatSenderStatus.mockReset()
|
||||
getPersonalWechatRuntimeStatus.mockReset().mockResolvedValue(null)
|
||||
checkPersonalWechatSenderStatus.mockReset().mockResolvedValue(windowsStatus)
|
||||
Object.defineProperty(window, 'api', {
|
||||
configurable: true,
|
||||
value: {
|
||||
getTextToSpeechSettings,
|
||||
listTextToSpeechVoices,
|
||||
openFishAudioApiKeys,
|
||||
getSettings,
|
||||
setSettings,
|
||||
getPersonalWechatSenderStatus,
|
||||
getPersonalWechatRuntimeStatus,
|
||||
checkPersonalWechatSenderStatus
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps an unconfigured port empty and saves it only after a successful check', async () => {
|
||||
const onNotice = vi.fn()
|
||||
render(<PersonalWechatSendPage onNotice={onNotice} />)
|
||||
|
||||
const portInput = await screen.findByRole('spinbutton', { name: '微信发送能力端口' })
|
||||
expect(portInput).toHaveValue(null)
|
||||
expect(screen.getByRole('button', { name: '检测' })).toBeEnabled()
|
||||
expect(getPersonalWechatSenderStatus).not.toHaveBeenCalled()
|
||||
|
||||
fireEvent.change(portInput, { target: { value: '4567' } })
|
||||
fireEvent.click(screen.getByRole('button', { name: '检测' }))
|
||||
|
||||
await waitFor(() => expect(checkPersonalWechatSenderStatus).toHaveBeenCalledWith('4567'))
|
||||
expect(screen.getByRole('button', { name: '保存' })).toBeEnabled()
|
||||
expect(portInput).toHaveValue(4567)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '保存' }))
|
||||
await waitFor(() => expect(setSettings).toHaveBeenCalledWith({ windowsWechatPort: '4567' }))
|
||||
expect(screen.getByRole('button', { name: '检测' })).toBeEnabled()
|
||||
expect(onNotice).toHaveBeenCalledWith('微信发送能力端口已保存')
|
||||
})
|
||||
|
||||
it('clears a saved port and resets the connection state', async () => {
|
||||
getSettings.mockResolvedValue({ settings: { windowsWechatPort: '4567' } })
|
||||
getPersonalWechatSenderStatus.mockResolvedValue(windowsStatus)
|
||||
const onNotice = vi.fn()
|
||||
render(<PersonalWechatSendPage onNotice={onNotice} />)
|
||||
|
||||
const portInput = await screen.findByRole('spinbutton', { name: '微信发送能力端口' })
|
||||
expect(portInput).toHaveValue(4567)
|
||||
expect(screen.getByRole('button', { name: '清除端口' })).toBeEnabled()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '清除端口' }))
|
||||
|
||||
await waitFor(() => expect(setSettings).toHaveBeenCalledWith({ windowsWechatPort: '' }))
|
||||
expect(portInput).toHaveValue(null)
|
||||
expect(screen.getByRole('button', { name: '清除端口' })).toBeDisabled()
|
||||
expect(screen.getByRole('button', { name: '检测' })).toBeEnabled()
|
||||
expect(onNotice).toHaveBeenCalledWith('微信发送能力端口已清除')
|
||||
})
|
||||
|
||||
it('keeps the Windows send dialog free of macOS OneBot controls', async () => {
|
||||
getPersonalWechatSenderStatus.mockResolvedValue({
|
||||
...windowsStatus,
|
||||
state: 'error',
|
||||
canSend: false,
|
||||
canSendText: false,
|
||||
canSendImage: false,
|
||||
canSendVoice: false,
|
||||
message: '未检测到 Windows 微信发送能力,请先启动并登录微信'
|
||||
})
|
||||
|
||||
render(
|
||||
<PersonalWechatSendDialog
|
||||
contact={{
|
||||
m_nsUsrName: 'fixture-user',
|
||||
m_nsNickName: '文件传输助手',
|
||||
md5: 'fixture-md5',
|
||||
type: 'user'
|
||||
}}
|
||||
isGroupChat={false}
|
||||
onClose={vi.fn()}
|
||||
onOpenPersonalWechatSettings={vi.fn()}
|
||||
/>
|
||||
)
|
||||
|
||||
expect(
|
||||
await screen.findByText('未检测到 Windows 微信发送能力,请先启动并登录微信')
|
||||
).toBeVisible()
|
||||
expect(getPersonalWechatRuntimeStatus).not.toHaveBeenCalled()
|
||||
expect(screen.queryByText(/OneBot/i)).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('switch', { name: '保留 OneBot 进程' })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: '语音发送诊断' })).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('heading', { name: '个人微信发送' })).toBeVisible()
|
||||
expect(screen.getByRole('region', { name: '发送消息' })).toBeVisible()
|
||||
})
|
||||
|
||||
it('opens the Windows send dialog in text mode by default', async () => {
|
||||
getPersonalWechatSenderStatus.mockResolvedValue(windowsStatus)
|
||||
|
||||
render(
|
||||
<PersonalWechatSendDialog
|
||||
contact={{
|
||||
m_nsUsrName: 'fixture-user',
|
||||
m_nsNickName: '文件传输助手',
|
||||
md5: 'fixture-md5',
|
||||
type: 'user'
|
||||
}}
|
||||
isGroupChat={false}
|
||||
onClose={vi.fn()}
|
||||
/>
|
||||
)
|
||||
|
||||
expect(await screen.findByRole('radio', { name: '文字' })).toHaveAttribute(
|
||||
'data-state',
|
||||
'checked'
|
||||
)
|
||||
expect(screen.getByRole('textbox', { name: '消息内容' })).toBeVisible()
|
||||
})
|
||||
|
||||
it('keeps the Windows voice page focused on speech settings', async () => {
|
||||
render(<TextToSpeechPage onNotice={vi.fn()} />)
|
||||
|
||||
expect(await screen.findByText('API 设置')).toBeVisible()
|
||||
expect(screen.queryByText(/OneBot/i)).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('微信发送组件')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -14,10 +14,6 @@ const getSettings = vi.fn()
|
||||
const saveSettings = vi.fn()
|
||||
const listVoices = vi.fn()
|
||||
const openApiKeys = vi.fn()
|
||||
const getRuntimeStatus = vi.fn()
|
||||
const onRuntimeProgress = vi.fn(() => vi.fn())
|
||||
const openRuntimeDirectory = vi.fn()
|
||||
const removeRuntime = vi.fn()
|
||||
|
||||
const response = {
|
||||
success: true,
|
||||
@@ -55,7 +51,6 @@ const response = {
|
||||
|
||||
describe('TextToSpeechPage', () => {
|
||||
beforeEach(() => {
|
||||
sessionStorage.clear()
|
||||
getSettings.mockReset().mockResolvedValue(response)
|
||||
listVoices.mockReset().mockResolvedValue({
|
||||
success: true,
|
||||
@@ -75,42 +70,13 @@ describe('TextToSpeechPage', () => {
|
||||
}
|
||||
}))
|
||||
openApiKeys.mockReset().mockResolvedValue({ success: true })
|
||||
getRuntimeStatus.mockReset().mockResolvedValue({
|
||||
version: 'v0.0.18',
|
||||
state: 'ready',
|
||||
downloadedBytes: 100,
|
||||
totalBytes: 100,
|
||||
progress: 1,
|
||||
platform: 'darwin',
|
||||
architecture: 'arm64',
|
||||
supported: true,
|
||||
removable: true,
|
||||
directory: '/tmp/personal-wechat-runtime'
|
||||
})
|
||||
onRuntimeProgress.mockReset().mockReturnValue(vi.fn())
|
||||
openRuntimeDirectory.mockReset().mockResolvedValue({ success: true })
|
||||
removeRuntime.mockReset().mockResolvedValue({
|
||||
version: 'v0.0.18',
|
||||
state: 'missing',
|
||||
downloadedBytes: 0,
|
||||
totalBytes: 100,
|
||||
progress: 0,
|
||||
platform: 'darwin',
|
||||
architecture: 'arm64',
|
||||
supported: true,
|
||||
removable: false
|
||||
})
|
||||
Object.defineProperty(window, 'api', {
|
||||
configurable: true,
|
||||
value: {
|
||||
getTextToSpeechSettings: getSettings,
|
||||
saveTextToSpeechSettings: saveSettings,
|
||||
listTextToSpeechVoices: listVoices,
|
||||
openFishAudioApiKeys: openApiKeys,
|
||||
getPersonalWechatRuntimeStatus: getRuntimeStatus,
|
||||
onPersonalWechatRuntimeProgress: onRuntimeProgress,
|
||||
openPersonalWechatRuntimeDirectory: openRuntimeDirectory,
|
||||
removePersonalWechatRuntime: removeRuntime
|
||||
openFishAudioApiKeys: openApiKeys
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -148,7 +114,7 @@ describe('TextToSpeechPage', () => {
|
||||
|
||||
it('opens the official Fish Audio API key page through the main process', async () => {
|
||||
render(<TextToSpeechPage onNotice={vi.fn()} />)
|
||||
await screen.findByText('微信发送组件')
|
||||
await screen.findByText('API 设置')
|
||||
fireEvent.click(screen.getByRole('button', { name: '前往 api.fish.audio 获取 Key' }))
|
||||
await waitFor(() => expect(openApiKeys).toHaveBeenCalledTimes(1))
|
||||
})
|
||||
@@ -170,49 +136,11 @@ describe('TextToSpeechPage', () => {
|
||||
await waitFor(() => expect(saveSettings).toHaveBeenCalledWith({ clearApiKey: true }))
|
||||
})
|
||||
|
||||
it('keeps runtime directory and refresh actions wired to the existing API', async () => {
|
||||
const user = userEvent.setup()
|
||||
it('keeps the speech page free of personal WeChat runtime controls', async () => {
|
||||
render(<TextToSpeechPage onNotice={vi.fn()} />)
|
||||
|
||||
await user.click(await screen.findByRole('button', { name: '打开目录' }))
|
||||
expect(openRuntimeDirectory).toHaveBeenCalledOnce()
|
||||
await user.click(screen.getByRole('button', { name: '重新检测' }))
|
||||
expect(getRuntimeStatus).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('opens supported versions from both triggers and restores focus after closing', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<TextToSpeechPage onNotice={vi.fn()} />)
|
||||
|
||||
const guideTrigger = await screen.findByRole('button', { name: '查看支持版本' })
|
||||
await user.click(guideTrigger)
|
||||
expect(screen.getByRole('dialog', { name: '支持的微信版本' })).toBeVisible()
|
||||
expect(screen.getByText('请安装下列完整版本之一。')).toBeVisible()
|
||||
expect(screen.getByRole('link', { name: /下载微信历史版本/ })).toHaveAttribute(
|
||||
'href',
|
||||
'https://github.com/zsbai/wechat-versions/releases'
|
||||
)
|
||||
expect(screen.getByText('4.1.6.12')).toBeVisible()
|
||||
expect(screen.getByText('4.1.11.53')).toBeVisible()
|
||||
|
||||
await user.keyboard('{Escape}')
|
||||
expect(screen.queryByRole('dialog', { name: '支持的微信版本' })).not.toBeInTheDocument()
|
||||
expect(guideTrigger).toHaveFocus()
|
||||
|
||||
const runtimeTrigger = screen.getByRole('button', { name: '支持版本' })
|
||||
await user.click(runtimeTrigger)
|
||||
const dialog = screen.getByRole('dialog', { name: '支持的微信版本' })
|
||||
expect(dialog).toBeVisible()
|
||||
await user.click(dialog.previousElementSibling as HTMLElement)
|
||||
expect(screen.queryByRole('dialog', { name: '支持的微信版本' })).not.toBeInTheDocument()
|
||||
expect(runtimeTrigger).toHaveFocus()
|
||||
})
|
||||
|
||||
it('keeps the session handoff that opens supported versions automatically', async () => {
|
||||
sessionStorage.setItem('wxe:show-supported-wechat-versions', '1')
|
||||
render(<TextToSpeechPage onNotice={vi.fn()} />)
|
||||
|
||||
expect(await screen.findByRole('dialog', { name: '支持的微信版本' })).toBeVisible()
|
||||
expect(sessionStorage.getItem('wxe:show-supported-wechat-versions')).toBeNull()
|
||||
expect(await screen.findByText('API 设置')).toBeVisible()
|
||||
expect(screen.queryByText(/微信|OneBot/i)).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('支持的微信版本')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -260,9 +260,9 @@ test('SETTINGS-01 supported WeChat versions dialog is keyboard-safe and fits the
|
||||
.getByRole('navigation', { name: '一级导航' })
|
||||
.getByRole('button', { name: '设置' })
|
||||
.click()
|
||||
await fixture.page.getByRole('button', { name: '文字转语音' }).click()
|
||||
await fixture.page.getByRole('button', { name: '微信发送' }).click()
|
||||
|
||||
const trigger = fixture.page.getByRole('button', { name: '查看支持版本' })
|
||||
const trigger = fixture.page.getByRole('button', { name: '支持版本' })
|
||||
await expect(trigger).toBeVisible()
|
||||
await trigger.click()
|
||||
const dialog = fixture.page.getByRole('dialog', { name: '支持的微信版本' })
|
||||
@@ -278,6 +278,7 @@ test('SETTINGS-01 supported WeChat versions dialog is keyboard-safe and fits the
|
||||
await expect(dialog).toHaveCount(0)
|
||||
await expect(trigger).toBeFocused()
|
||||
|
||||
await fixture.page.getByRole('button', { name: '文字转语音' }).click()
|
||||
const modelSelect = fixture.page.getByRole('combobox', { name: '合成模型' })
|
||||
await modelSelect.scrollIntoViewIfNeeded()
|
||||
await modelSelect.click()
|
||||
|
||||
@@ -98,6 +98,8 @@ describe('preload IPC contract', () => {
|
||||
|
||||
await api.getPersonalWechatSenderStatus()
|
||||
expect(invoke).toHaveBeenLastCalledWith('wechat-personal:getStatus')
|
||||
await api.checkPersonalWechatSenderStatus('4567')
|
||||
expect(invoke).toHaveBeenLastCalledWith('wechat-personal:checkStatus', '4567')
|
||||
await api.rebindPersonalWechatSender()
|
||||
expect(invoke).toHaveBeenLastCalledWith('wechat-personal:rebind')
|
||||
const sendRequest = {
|
||||
|
||||
@@ -31,12 +31,25 @@ const status = (
|
||||
|
||||
describe('PersonalWechatCapabilityService', () => {
|
||||
it.each([
|
||||
['unsupported', status({ platform: 'win32' })],
|
||||
['unsupported', status({ platform: 'linux' })],
|
||||
['unconfigured', status({ runtimeReady: false, boundWechatPid: undefined })],
|
||||
['needs_binding', status({ runtimeReady: true, boundWechatPid: undefined })],
|
||||
['needs_verification', status({ boundWechatPid: 123 })],
|
||||
['ready', status({ state: 'online', boundWechatPid: 123, canSendImage: true, canSend: true })],
|
||||
['error', status({ state: 'error', boundWechatPid: 123, error: 'hook failed' })]
|
||||
['error', status({ state: 'error', boundWechatPid: 123, error: 'hook failed' })],
|
||||
['needs_verification', status({ platform: 'win32', endpoint: '127.0.0.1:4567' })],
|
||||
[
|
||||
'ready',
|
||||
status({
|
||||
platform: 'win32',
|
||||
state: 'online',
|
||||
endpoint: '127.0.0.1:4567',
|
||||
canSend: true,
|
||||
canSendText: true,
|
||||
canSendImage: true,
|
||||
canSendVoice: true
|
||||
})
|
||||
]
|
||||
])('maps %s', (expected, senderStatus) => {
|
||||
const service = new PersonalWechatCapabilityService({ getStatus: async () => senderStatus })
|
||||
return expect(service.getPersonalWechatSendCapability()).resolves.toMatchObject({
|
||||
|
||||
@@ -13,9 +13,13 @@ vi.mock('electron', () => ({
|
||||
|
||||
import {
|
||||
buildPersonalWechatVoiceDiagnostic,
|
||||
buildWindowsWechatRequest,
|
||||
buildPersonalWechatOneBotRequest,
|
||||
findWechatImagePath,
|
||||
findPersonalWechatRuntime,
|
||||
normalizeWindowsWechatPort,
|
||||
parseWindowsLoginStatus,
|
||||
parseWindowsHookResponse,
|
||||
parsePersonalWechatHookLog
|
||||
} from '../../src/main/services/personal-wechat-send-service'
|
||||
|
||||
@@ -130,6 +134,91 @@ describe('personal WeChat OneBot request', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Windows request', () => {
|
||||
it('accepts only a valid configured local port', () => {
|
||||
expect(normalizeWindowsWechatPort('')).toBeNull()
|
||||
expect(normalizeWindowsWechatPort(' 4567 ')).toBe('4567')
|
||||
expect(normalizeWindowsWechatPort('0')).toBeNull()
|
||||
expect(normalizeWindowsWechatPort('65536')).toBeNull()
|
||||
expect(normalizeWindowsWechatPort('port')).toBeNull()
|
||||
})
|
||||
|
||||
it('requires ret=0 for message responses but accepts the status shape', () => {
|
||||
expect(parseWindowsHookResponse('{"ret":0,"retmsg":"success"}', true)).toEqual({
|
||||
ret: 0,
|
||||
retmsg: 'success'
|
||||
})
|
||||
expect(parseWindowsHookResponse('{"IsLogin":0,"hWeixin":123}', false)).toEqual({
|
||||
IsLogin: 0,
|
||||
hWeixin: 123
|
||||
})
|
||||
expect(() => parseWindowsHookResponse('{"ret":1,"retmsg":"fail"}', true)).toThrow('fail')
|
||||
expect(() => parseWindowsHookResponse('{"retmsg":"success"}', true)).toThrow('success')
|
||||
expect(() => parseWindowsHookResponse('{}', true)).toThrow('ret=undefined')
|
||||
expect(() => parseWindowsHookResponse('', true)).toThrow('空响应')
|
||||
})
|
||||
|
||||
it('allows Windows sending only for a strict logged-in status', () => {
|
||||
expect(parseWindowsLoginStatus({ status: true })).toBe(true)
|
||||
expect(parseWindowsLoginStatus({ status: false })).toBe(false)
|
||||
expect(parseWindowsLoginStatus({ status: 1 })).toBe(false)
|
||||
expect(parseWindowsLoginStatus({ status: 'true' })).toBe(false)
|
||||
expect(parseWindowsLoginStatus({ IsLogin: 1, hWeixin: 123 })).toBe(false)
|
||||
})
|
||||
|
||||
it('builds the unified text request', () => {
|
||||
expect(
|
||||
buildWindowsWechatRequest({
|
||||
to: ' wxid_fixture ',
|
||||
type: 'text',
|
||||
text: ' 测试发送 ',
|
||||
isGroup: false
|
||||
})
|
||||
).toEqual({
|
||||
endpoint: '/SendMsg',
|
||||
body: { toWxid: 'wxid_fixture', type: 'text', msg: '测试发送' }
|
||||
})
|
||||
})
|
||||
|
||||
it('builds the unified image request with a local path', () => {
|
||||
expect(
|
||||
buildWindowsWechatRequest({
|
||||
to: 'room@chatroom',
|
||||
type: 'image',
|
||||
filePath: 'C:\\fixture\\image.png',
|
||||
isGroup: true
|
||||
})
|
||||
).toEqual({
|
||||
endpoint: '/SendMsg',
|
||||
body: { toWxid: 'room@chatroom', type: 'image', msg: 'C:\\fixture\\image.png' }
|
||||
})
|
||||
})
|
||||
|
||||
it('builds the unified voice request with sender wxid and duration', () => {
|
||||
expect(
|
||||
buildWindowsWechatRequest(
|
||||
{
|
||||
to: 'wxid_fixture',
|
||||
type: 'voice',
|
||||
filePath: '/source/input.wav',
|
||||
fromId: ' wxid_self ',
|
||||
isGroup: false
|
||||
},
|
||||
{ filePath: 'C:\\Temp\\prepared.silk', durationMs: 1234 }
|
||||
)
|
||||
).toEqual({
|
||||
endpoint: '/SendMsg',
|
||||
body: {
|
||||
toWxid: 'wxid_fixture',
|
||||
type: 'voice',
|
||||
msg: 'C:\\Temp\\prepared.silk',
|
||||
fromWxid: 'wxid_self',
|
||||
duration: 1234
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('personal WeChat runtime discovery', () => {
|
||||
it('uses the newest current-month WeChat image directory', () => {
|
||||
const root = temporaryDirectory()
|
||||
|
||||
@@ -8,6 +8,7 @@ describe('settings navigation', () => {
|
||||
it('places text-to-speech under intelligent capabilities', () => {
|
||||
const intelligent = SETTINGS_NAVIGATION.find((group) => group.label === '智能能力')
|
||||
expect(intelligent?.items.map((item) => item.id)).toEqual([
|
||||
'wechat-send',
|
||||
'voice-recognition',
|
||||
'text-to-speech',
|
||||
'ai-model'
|
||||
|
||||
Reference in New Issue
Block a user