Browse Source

✨ feat(sdk): 添加SDK功能测试页面和端到端测试

- 新增SDK功能测试页面,提供完整的SDK集成测试环境
- 添加Playwright端到端测试配置和测试用例
- 在登录页面添加SDK测试入口链接
- 优化SDK配置接口,增加证书(certificate)必填验证
- 更新相关测试用例以适配新的配置要求

📦 build(scripts): 添加Playwright相关脚本

- 在package.json中添加Playwright测试脚本
- 添加Playwright浏览器安装脚本
- 支持多浏览器测试和调试模式

✅ test(e2e): 添加端到端测试套件

- 添加SDK测试页面的完整端到端测试
- 覆盖SDK初始化、管理器创建、转录功能等核心流程
- 验证错误处理和资源清理功能
yourname 2 months ago
parent
commit
e3696b28c7

+ 10 - 2
package.json

@@ -15,7 +15,14 @@
     "lint:fix": "npm run lint --fix",
     "prettier": "prettier . --write --ignore-unknown",
     "preview": "vite preview",
-    "proto": "pbjs -t json-module -w es6 --es6 -o src/protobuf/SttMessage.js src/protobuf/SttMessage.proto"
+    "proto": "pbjs -t json-module -w es6 --es6 -o src/protobuf/SttMessage.js src/protobuf/SttMessage.proto",
+    "test:e2e": "playwright test",
+    "test:e2e:chromium": "playwright test --project=chromium",
+    "test:e2e:firefox": "playwright test --project=firefox",
+    "test:e2e:webkit": "playwright test --project=webkit",
+    "test:e2e:ui": "playwright test --ui",
+    "test:e2e:debug": "playwright test --debug",
+    "playwright:install": "playwright install"
   },
   "sideEffects": [
     "*.css"
@@ -64,7 +71,8 @@
     "typescript": "^5.2.2",
     "vite": "^5.0.8",
     "yorkie": "^2.0.0",
-    "protobufjs-cli": "^1.1.2"
+    "protobufjs-cli": "^1.1.2",
+    "@playwright/test": "^1.55.0"
   },
   "gitHooks": {
     "pre-commit": "lint-staged"

+ 7 - 3
packages/stt-sdk-core/src/core/stt-sdk.ts

@@ -29,12 +29,16 @@ export class SttSdk
         throw new SttError('ALREADY_JOINED', 'SDK is already initialized')
       }
 
-      const { appId } = config
+      const { appId, certificate } = config
 
       if (!appId) {
         throw new SttError('INVALID_CONFIG', 'App ID is required')
       }
 
+      if (!certificate) {
+        throw new SttError('INVALID_CONFIG', 'Certificate is required')
+      }
+
       this._config = config
 
       // 模拟SDK初始化过程
@@ -59,7 +63,7 @@ export class SttSdk
       throw new SttError('NOT_INITIALIZED', 'SDK must be initialized before creating managers')
     }
 
-    const manager = new SttManagerAdapter(undefined, this._config?.appId)
+    const manager = new SttManagerAdapter(undefined, this._config!.appId, this._config!.certificate)
     this._sttManagers.add(manager)
 
     // 监听管理器错误事件并转发
@@ -75,7 +79,7 @@ export class SttSdk
       throw new SttError('NOT_INITIALIZED', 'SDK must be initialized before creating managers')
     }
 
-    const manager = new RtmManagerAdapter(this._config?.appId)
+    const manager = new RtmManagerAdapter(this._config!.appId)
     this._rtmManagers.add(manager)
 
     // 监听管理器错误事件并转发

+ 5 - 5
packages/stt-sdk-core/src/managers/stt-manager-adapter.ts

@@ -54,15 +54,15 @@ export class SttManagerAdapter extends AGEventEmitter<SttEventMap> implements IS
   private _rtmManager?: any
   private _option?: { token: string; taskId: string }
   private _appId: string = ''
+  private _certificate: string = ''
   private _gatewayAddress = 'https://api.agora.io'
   private _baseUrl = 'https://service.agora.io/toolbox-overseas'
 
-  constructor(rtmManager?: any, appId?: string) {
+  constructor(rtmManager?: any, appId: string, certificate: string) {
     super()
     this._rtmManager = rtmManager
-    if (appId) {
-      this._appId = appId
-    }
+    this._appId = appId
+    this._certificate = certificate
   }
 
   async init(config: SttManagerConfig): Promise<void> {
@@ -510,7 +510,7 @@ export class SttManagerAdapter extends AGEventEmitter<SttEventMap> implements IS
 
     const data = {
       appId: this._appId,
-      appCertificate: '', // 在实际实现中需要提供证书
+      appCertificate: this._certificate,
       channelName: channel,
       expire: 7200,
       src: 'web',

+ 1 - 0
packages/stt-sdk-core/src/types/index.ts

@@ -130,6 +130,7 @@ export interface ISttSdk {
 
 export interface SttSdkConfig {
   appId: string
+  certificate: string
   token?: string
   logLevel?: 'debug' | 'info' | 'warn' | 'error'
 }

+ 9 - 0
packages/stt-sdk-core/tests/compatibility/compatibility.test.ts

@@ -19,17 +19,20 @@ describe('SDK Compatibility Test', () => {
   test('SDK should initialize successfully', async () => {
     await sdk.initialize({
       appId: 'test-app-id',
+      certificate: 'test-certificate',
       logLevel: 'info',
     })
 
     expect(sdk.isInitialized).toBe(true)
     expect(sdk.config?.appId).toBe('test-app-id')
+    expect(sdk.config?.certificate).toBe('test-certificate')
   })
 
   test('Should create STT manager with compatible API', async () => {
     // 先初始化SDK
     await sdk.initialize({
       appId: 'test-app-id',
+      certificate: 'test-certificate',
       logLevel: 'info',
     })
 
@@ -53,6 +56,7 @@ describe('SDK Compatibility Test', () => {
     // 先初始化SDK
     await sdk.initialize({
       appId: 'test-app-id',
+      certificate: 'test-certificate',
       logLevel: 'info',
     })
 
@@ -76,6 +80,7 @@ describe('SDK Compatibility Test', () => {
     // 先初始化SDK
     await sdk.initialize({
       appId: 'test-app-id',
+      certificate: 'test-certificate',
       logLevel: 'info',
     })
 
@@ -96,6 +101,7 @@ describe('SDK Compatibility Test', () => {
     // 先初始化SDK
     await sdk.initialize({
       appId: 'test-app-id',
+      certificate: 'test-certificate',
       logLevel: 'info',
     })
 
@@ -136,6 +142,7 @@ describe('SDK Compatibility Test', () => {
     // 先初始化SDK
     await sdk.initialize({
       appId: 'test-app-id',
+      certificate: 'test-certificate',
       logLevel: 'info',
     })
 
@@ -198,6 +205,7 @@ describe('SDK Compatibility Test', () => {
     // 先初始化SDK
     await sdk.initialize({
       appId: 'test-app-id',
+      certificate: 'test-certificate',
       logLevel: 'info',
     })
 
@@ -230,6 +238,7 @@ describe('SDK Compatibility Test', () => {
     // 1. 创建管理器实例
     await sdk.initialize({
       appId: 'test-app-id',
+      certificate: 'test-certificate',
       logLevel: 'info',
     })
     const sttManager = sdk.createSttManager()

+ 1 - 0
packages/stt-sdk-core/tests/compatibility/integration.test.ts

@@ -13,6 +13,7 @@ describe('Integration Test - Existing Application Pattern', () => {
     sdk = new SttSdk()
     await sdk.initialize({
       appId: 'test-app-id',
+      certificate: 'test-certificate',
       logLevel: 'info',
     })
 

+ 29 - 8
packages/stt-sdk-core/tests/core/stt-sdk.test.ts

@@ -17,7 +17,7 @@ describe('SttSdk', () => {
 
   describe('initialize', () => {
     it('should initialize successfully with valid config', async () => {
-      const config = { appId: 'test-app-id' }
+      const config = { appId: 'test-app-id', certificate: 'test-certificate' }
 
       await sdk.initialize(config)
 
@@ -26,14 +26,21 @@ describe('SttSdk', () => {
     })
 
     it('should throw error when appId is missing', async () => {
-      const config = { appId: '' }
+      const config = { appId: '', certificate: 'test-certificate' }
 
       await expect(sdk.initialize(config)).rejects.toThrow(SttError)
       await expect(sdk.initialize(config)).rejects.toThrow('App ID is required')
     })
 
+    it('should throw error when certificate is missing', async () => {
+      const config = { appId: 'test-app-id', certificate: '' }
+
+      await expect(sdk.initialize(config)).rejects.toThrow(SttError)
+      await expect(sdk.initialize(config)).rejects.toThrow('Certificate is required')
+    })
+
     it('should throw error when already initialized', async () => {
-      const config = { appId: 'test-app-id' }
+      const config = { appId: 'test-app-id', certificate: 'test-certificate' }
 
       await sdk.initialize(config)
 
@@ -44,7 +51,7 @@ describe('SttSdk', () => {
 
   describe('createSttManager', () => {
     it('should create STT manager when SDK is initialized', async () => {
-      await sdk.initialize({ appId: 'test-app-id' })
+      await sdk.initialize({ appId: 'test-app-id', certificate: 'test-certificate' })
 
       const manager = sdk.createSttManager()
 
@@ -62,7 +69,7 @@ describe('SttSdk', () => {
 
   describe('createRtmManager', () => {
     it('should create RTM manager when SDK is initialized', async () => {
-      await sdk.initialize({ appId: 'test-app-id' })
+      await sdk.initialize({ appId: 'test-app-id', certificate: 'test-certificate' })
 
       const manager = sdk.createRtmManager()
 
@@ -80,7 +87,7 @@ describe('SttSdk', () => {
 
   describe('destroy', () => {
     it('should destroy SDK successfully', async () => {
-      await sdk.initialize({ appId: 'test-app-id' })
+      await sdk.initialize({ appId: 'test-app-id', certificate: 'test-certificate' })
 
       // 创建管理器以测试销毁功能
       sdk.createSttManager()
@@ -104,7 +111,7 @@ describe('SttSdk', () => {
       const initializedHandler = vi.fn()
       sdk.on('initialized', initializedHandler)
 
-      await sdk.initialize({ appId: 'test-app-id' })
+      await sdk.initialize({ appId: 'test-app-id', certificate: 'test-certificate' })
 
       expect(initializedHandler).toHaveBeenCalledTimes(1)
     })
@@ -114,7 +121,21 @@ describe('SttSdk', () => {
       sdk.on('error', errorHandler)
 
       try {
-        await sdk.initialize({ appId: '' })
+        await sdk.initialize({ appId: '', certificate: 'test-certificate' })
+      } catch (error) {
+        // Expected to throw
+      }
+
+      expect(errorHandler).toHaveBeenCalledTimes(1)
+      expect(errorHandler.mock.calls[0][0]).toBeInstanceOf(SttError)
+    })
+
+    it('should emit error event when certificate is missing', async () => {
+      const errorHandler = vi.fn()
+      sdk.on('error', errorHandler)
+
+      try {
+        await sdk.initialize({ appId: 'test-app-id', certificate: '' })
       } catch (error) {
         // Expected to throw
       }

+ 5 - 4
packages/stt-sdk-core/tests/managers/stt-manager-adapter.test.ts

@@ -7,6 +7,7 @@ describe('SttManagerAdapter', () => {
   let manager: SttManagerAdapter
   let mockRtmManager: RtmManagerAdapter
   const mockAppId = 'test-app-id'
+  const mockCertificate = 'test-certificate'
 
   beforeEach(() => {
     // 创建模拟的 RTM 管理器
@@ -27,7 +28,7 @@ describe('SttManagerAdapter', () => {
       emit: vi.fn().mockReturnThis(),
     } as any
 
-    manager = new SttManagerAdapter(mockRtmManager, mockAppId)
+    manager = new SttManagerAdapter(mockRtmManager, mockAppId, mockCertificate)
   })
 
   afterEach(async () => {
@@ -71,7 +72,7 @@ describe('SttManagerAdapter', () => {
     })
 
     it('should throw error when appId is not provided', async () => {
-      const managerWithoutAppId = new SttManagerAdapter(mockRtmManager)
+      const managerWithoutAppId = new SttManagerAdapter(mockRtmManager, '', 'test-certificate')
       const config = {
         userId: 'test-user',
         channel: 'test-channel',
@@ -229,7 +230,7 @@ describe('SttManagerAdapter', () => {
     })
 
     it('should throw error when not initialized', async () => {
-      const uninitializedManager = new SttManagerAdapter(mockRtmManager, mockAppId)
+      const uninitializedManager = new SttManagerAdapter(mockRtmManager, mockAppId, mockCertificate)
 
       await expect(uninitializedManager.stopTranscription()).rejects.toThrow(SttError)
       await expect(uninitializedManager.stopTranscription()).rejects.toThrow(
@@ -238,7 +239,7 @@ describe('SttManagerAdapter', () => {
     })
 
     it('should throw error when no active task found', async () => {
-      const managerWithoutTask = new SttManagerAdapter(mockRtmManager, mockAppId)
+      const managerWithoutTask = new SttManagerAdapter(mockRtmManager, mockAppId, mockCertificate)
       await managerWithoutTask.init({
         userId: 'test-user',
         channel: 'test-channel',

+ 78 - 0
playwright.config.ts

@@ -0,0 +1,78 @@
+import { defineConfig, devices } from "@playwright/test"
+
+/**
+ * Read environment variables from file.
+ * https://github.com/motdotla/dotenv
+ */
+// import dotenv from 'dotenv';
+// dotenv.config({ path: path.resolve(__dirname, '.env') });
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+  testDir: "./tests/e2e",
+  /* Run tests in files in parallel */
+  fullyParallel: true,
+  /* Fail the build on CI if you accidentally left test.only in the source code. */
+  forbidOnly: !!process.env.CI,
+  /* Retry on CI only */
+  retries: process.env.CI ? 2 : 0,
+  /* Opt out of parallel tests on CI. */
+  workers: process.env.CI ? 1 : undefined,
+  /* Reporter to use. See https://playwright.dev/docs/test-reporters */
+  reporter: "html",
+  /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
+  use: {
+    /* Base URL to use in actions like `await page.goto('/')`. */
+    baseURL: "http://localhost:8080",
+
+    /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
+    trace: "on-first-retry",
+  },
+
+  /* Configure projects for major browsers */
+  projects: [
+    {
+      name: "chromium",
+      use: { ...devices["Desktop Chrome"] },
+    },
+
+    {
+      name: "firefox",
+      use: { ...devices["Desktop Firefox"] },
+    },
+
+    {
+      name: "webkit",
+      use: { ...devices["Desktop Safari"] },
+    },
+
+    /* Test against mobile viewports. */
+    // {
+    //   name: 'Mobile Chrome',
+    //   use: { ...devices['Pixel 5'] },
+    // },
+    // {
+    //   name: 'Mobile Safari',
+    //   use: { ...devices['iPhone 12'] },
+    // },
+
+    /* Test against branded browsers. */
+    // {
+    //   name: 'Microsoft Edge',
+    //   use: { ...devices['Desktop Edge'], channel: 'msedge' },
+    // },
+    // {
+    //   name: 'Google Chrome',
+    //   use: { ...devices['Desktop Chrome'], channel: 'chrome' },
+    // },
+  ],
+
+  /* Run your local dev server before starting the tests */
+  webServer: {
+    command: "npm run dev",
+    url: "http://localhost:8080",
+    reuseExistingServer: !process.env.CI,
+  },
+})

+ 19 - 5
src/pages/login/index.module.scss

@@ -35,14 +35,13 @@
         align-items: center;
         gap: 4px;
         border-radius: 100px;
-        background: var(--Grey-200, #F2F4F7);
+        background: var(--Grey-200, #f2f4f7);
         cursor: pointer;
 
         &:hover {
-          background: var(--Primary-50-T, #ECEEFE);
+          background: var(--Primary-50-T, #eceefe);
         }
 
-
         img {
           width: 22px;
           height: 22px;
@@ -95,7 +94,7 @@
       }
     }
 
-    .item+.item {
+    .item + .item {
       margin-top: 24px;
     }
 
@@ -120,8 +119,23 @@
       cursor: pointer;
     }
 
+    .sdkTestLink {
+      margin-top: 16px;
+      text-align: center;
+
+      :global(.ant-btn-link) {
+        color: #1890ff;
+        font-size: 14px;
+        padding: 4px 8px;
+
+        &:hover {
+          color: #40a9ff;
+        }
+      }
+    }
+
     .version {
-      margin-top: 32px;
+      margin-top: 16px;
       color: #6d7278;
       font-size: 14px;
       font-style: normal;

+ 11 - 0
src/pages/login/index.tsx

@@ -77,6 +77,10 @@ const LoginPage = () => {
     window.open(GITHUB_URL, "_blank")
   }
 
+  const onClickSdkTest = () => {
+    nav("/sdk-test")
+  }
+
   return (
     <div className={styles.loginPage}>
       {contextHolder}
@@ -122,6 +126,13 @@ const LoginPage = () => {
         <div className={styles.btn} onClick={onClickJoin}>
           {t("login.join")}
         </div>
+
+        <div className={styles.sdkTestLink}>
+          <Button type="link" onClick={onClickSdkTest}>
+            🧪 进入 SDK 功能测试页面
+          </Button>
+        </div>
+
         <div className={styles.version}>Version {version}</div>
       </section>
     </div>

+ 92 - 0
src/pages/sdk-test/index.module.scss

@@ -0,0 +1,92 @@
+.sdkTestPage {
+  min-height: 100vh;
+  background: #f5f5f5;
+  padding: 24px;
+
+  .header {
+    text-align: center;
+    margin-bottom: 32px;
+
+    h2 {
+      margin-bottom: 8px;
+      color: #1890ff;
+    }
+  }
+
+  .content {
+    display: flex;
+    gap: 24px;
+    max-width: 1200px;
+    margin: 0 auto;
+
+    @media (max-width: 768px) {
+      flex-direction: column;
+    }
+
+    .leftPanel {
+      flex: 1;
+      display: flex;
+      flex-direction: column;
+      gap: 24px;
+
+      .configCard,
+      .testCard {
+        :global(.ant-card-body) {
+          padding: 24px;
+        }
+      }
+    }
+
+    .rightPanel {
+      width: 400px;
+      display: flex;
+      flex-direction: column;
+      gap: 24px;
+
+      @media (max-width: 768px) {
+        width: 100%;
+      }
+
+      .logCard {
+        flex: 1;
+
+        .logContainer {
+          height: 300px;
+          overflow-y: auto;
+          background: #fafafa;
+          border: 1px solid #d9d9d9;
+          border-radius: 6px;
+          padding: 12px;
+          font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
+          font-size: 12px;
+          line-height: 1.5;
+
+          .logEntry {
+            margin-bottom: 4px;
+            word-break: break-all;
+
+            &:last-child {
+              margin-bottom: 0;
+            }
+          }
+        }
+      }
+
+      .infoCard {
+        :global(.ant-card-body) {
+          padding: 16px;
+        }
+
+        ol {
+          margin: 8px 0;
+          padding-left: 20px;
+
+          li {
+            margin-bottom: 4px;
+            color: #666;
+          }
+        }
+      }
+    }
+  }
+}

+ 428 - 0
src/pages/sdk-test/index.tsx

@@ -0,0 +1,428 @@
+import { useState, useEffect } from "react"
+import { Card, Button, Input, Form, message, Space, Typography, Divider, Alert } from "antd"
+import {
+  InfoCircleOutlined,
+  PlayCircleOutlined,
+  StopOutlined,
+  ReloadOutlined,
+} from "@ant-design/icons"
+import { useNavigate } from "react-router-dom"
+import { genRandomUserId } from "@/common"
+
+import styles from "./index.module.scss"
+
+// 导入SDK核心模块
+import { SttSdk } from "../../../packages/stt-sdk-core/src"
+import type { ISttManagerAdapter, IRtmManagerAdapter } from "../../../packages/stt-sdk-core/src"
+
+const { Title, Text } = Typography
+
+const SdkTestPage = () => {
+  const nav = useNavigate()
+  const [messageApi, contextHolder] = message.useMessage()
+
+  const [form] = Form.useForm()
+  const [sdk, setSdk] = useState<SttSdk | null>(null)
+  const [sttManager, setSttManager] = useState<ISttManagerAdapter | null>(null)
+  const [rtmManager, setRtmManager] = useState<IRtmManagerAdapter | null>(null)
+  const [isSdkInitialized, setIsSdkInitialized] = useState(false)
+  const [isSttManagerInitialized, setIsSttManagerInitialized] = useState(false)
+  const [isRtmManagerJoined, setIsRtmManagerJoined] = useState(false)
+  const [isTranscriptionActive, setIsTranscriptionActive] = useState(false)
+  const [transcriptionStatus, setTranscriptionStatus] = useState<string>("idle")
+  const [testResults, setTestResults] = useState<string[]>([])
+
+  // 添加测试日志
+  const addTestLog = (log: string) => {
+    setTestResults((prev) => [...prev, `${new Date().toLocaleTimeString()}: ${log}`])
+  }
+
+  // 初始化SDK
+  const initializeSdk = async (values: {
+    appId: string
+    certificate: string
+    channel: string
+    userName: string
+  }) => {
+    try {
+      addTestLog("开始初始化SDK...")
+
+      const newSdk = new SttSdk()
+      await newSdk.initialize({
+        appId: values.appId,
+        certificate: values.certificate,
+        logLevel: "info",
+      })
+
+      setSdk(newSdk)
+      setIsSdkInitialized(true)
+      addTestLog("✅ SDK初始化成功")
+
+      // 创建管理器
+      const sttManager = newSdk.createSttManager()
+      const rtmManager = newSdk.createRtmManager()
+
+      setSttManager(sttManager)
+      setRtmManager(rtmManager)
+      addTestLog("✅ STT和RTM管理器创建成功")
+
+      messageApi.success("SDK初始化成功")
+    } catch (error) {
+      addTestLog(`❌ SDK初始化失败: ${error}`)
+      messageApi.error(`SDK初始化失败: ${error}`)
+    }
+  }
+
+  // 初始化STT管理器
+  const initializeSttManager = async () => {
+    if (!sttManager || !form) return
+
+    try {
+      const values = form.getFieldsValue()
+      addTestLog("开始初始化STT管理器...")
+
+      await sttManager.init({
+        userId: genRandomUserId(),
+        channel: values.channel,
+        userName: values.userName,
+      })
+
+      setIsSttManagerInitialized(true)
+      addTestLog("✅ STT管理器初始化成功")
+      messageApi.success("STT管理器初始化成功")
+    } catch (error) {
+      addTestLog(`❌ STT管理器初始化失败: ${error}`)
+      messageApi.error(`STT管理器初始化失败: ${error}`)
+    }
+  }
+
+  // 加入RTM频道
+  const joinRtmChannel = async () => {
+    if (!rtmManager || !form) return
+
+    try {
+      const values = form.getFieldsValue()
+      addTestLog("开始加入RTM频道...")
+
+      await rtmManager.join({
+        channel: values.channel,
+        userId: genRandomUserId().toString(),
+        userName: values.userName,
+      })
+
+      setIsRtmManagerJoined(true)
+      addTestLog("✅ RTM频道加入成功")
+      messageApi.success("RTM频道加入成功")
+    } catch (error) {
+      addTestLog(`❌ RTM频道加入失败: ${error}`)
+      messageApi.error(`RTM频道加入失败: ${error}`)
+    }
+  }
+
+  // 开始转录
+  const startTranscription = async () => {
+    if (!sttManager) return
+
+    try {
+      addTestLog("开始语音转录...")
+      setTranscriptionStatus("starting")
+
+      await sttManager.startTranscription({
+        languages: [{ source: "en-US", target: ["zh-CN"] }],
+      })
+
+      setIsTranscriptionActive(true)
+      setTranscriptionStatus("active")
+      addTestLog("✅ 语音转录已开始")
+      messageApi.success("语音转录已开始")
+    } catch (error) {
+      addTestLog(`❌ 语音转录启动失败: ${error}`)
+      setTranscriptionStatus("error")
+      messageApi.error(`语音转录启动失败: ${error}`)
+    }
+  }
+
+  // 停止转录
+  const stopTranscription = async () => {
+    if (!sttManager) return
+
+    try {
+      addTestLog("停止语音转录...")
+      setTranscriptionStatus("stopping")
+
+      await sttManager.stopTranscription()
+
+      setIsTranscriptionActive(false)
+      setTranscriptionStatus("stopped")
+      addTestLog("✅ 语音转录已停止")
+      messageApi.success("语音转录已停止")
+    } catch (error) {
+      addTestLog(`❌ 语音转录停止失败: ${error}`)
+      setTranscriptionStatus("error")
+      messageApi.error(`语音转录停止失败: ${error}`)
+    }
+  }
+
+  // 查询转录状态
+  const queryTranscription = async () => {
+    if (!sttManager) return
+
+    try {
+      addTestLog("查询转录状态...")
+
+      const result = await sttManager.queryTranscription()
+      addTestLog(`📊 转录状态查询结果: ${JSON.stringify(result)}`)
+      messageApi.info("转录状态查询完成,查看日志了解详情")
+    } catch (error) {
+      addTestLog(`❌ 转录状态查询失败: ${error}`)
+      messageApi.error(`转录状态查询失败: ${error}`)
+    }
+  }
+
+  // 清理资源
+  const cleanup = async () => {
+    try {
+      addTestLog("开始清理资源...")
+
+      if (isTranscriptionActive && sttManager) {
+        await stopTranscription()
+      }
+
+      if (sttManager) {
+        await sttManager.destroy()
+        setSttManager(null)
+        setIsSttManagerInitialized(false)
+      }
+
+      if (rtmManager) {
+        await rtmManager.destroy()
+        setRtmManager(null)
+        setIsRtmManagerJoined(false)
+      }
+
+      if (sdk) {
+        await sdk.destroy()
+        setSdk(null)
+        setIsSdkInitialized(false)
+      }
+
+      addTestLog("✅ 资源清理完成")
+      setTranscriptionStatus("idle")
+      messageApi.success("资源清理完成")
+    } catch (error) {
+      addTestLog(`❌ 资源清理失败: ${error}`)
+      messageApi.error(`资源清理失败: ${error}`)
+    }
+  }
+
+  // 返回主应用
+  const goToMainApp = () => {
+    nav("/home")
+  }
+
+  // 组件卸载时清理资源
+  useEffect(() => {
+    return () => {
+      if (sdk) {
+        cleanup()
+      }
+    }
+  }, [sdk])
+
+  return (
+    <div className={styles.sdkTestPage}>
+      {contextHolder}
+
+      <div className={styles.header}>
+        <Title level={2}>🎯 SDK 功能测试页面</Title>
+        <Text type="secondary">测试 STT SDK 核心功能的集成和兼容性</Text>
+      </div>
+
+      <div className={styles.content}>
+        <div className={styles.leftPanel}>
+          <Card title="🔧 SDK 配置" className={styles.configCard}>
+            <Form
+              form={form}
+              layout="vertical"
+              initialValues={{
+                appId: import.meta.env.VITE_AGORA_APP_ID || "",
+                certificate: "",
+                channel: `test-channel-${Date.now()}`,
+                userName: `TestUser-${Date.now()}`,
+              }}
+            >
+              <Form.Item
+                label="App ID"
+                name="appId"
+                rules={[{ required: true, message: "请输入 App ID" }]}
+              >
+                <Input placeholder="请输入 Agora App ID" />
+              </Form.Item>
+
+              <Form.Item
+                label="Certificate"
+                name="certificate"
+                rules={[{ required: true, message: "请输入 Certificate" }]}
+              >
+                <Input.Password placeholder="请输入 Agora Certificate" />
+              </Form.Item>
+
+              <Form.Item
+                label="频道名称"
+                name="channel"
+                rules={[{ required: true, message: "请输入频道名称" }]}
+              >
+                <Input placeholder="请输入频道名称" />
+              </Form.Item>
+
+              <Form.Item
+                label="用户名称"
+                name="userName"
+                rules={[{ required: true, message: "请输入用户名称" }]}
+              >
+                <Input placeholder="请输入用户名称" />
+              </Form.Item>
+
+              <Form.Item>
+                <Space>
+                  <Button
+                    type="primary"
+                    onClick={() => initializeSdk(form.getFieldsValue())}
+                    disabled={isSdkInitialized}
+                  >
+                    {isSdkInitialized ? "✅ 已初始化" : "初始化 SDK"}
+                  </Button>
+
+                  <Button onClick={cleanup} danger disabled={!isSdkInitialized}>
+                    清理资源
+                  </Button>
+                </Space>
+              </Form.Item>
+            </Form>
+          </Card>
+
+          <Card title="🚀 功能测试" className={styles.testCard}>
+            <Space direction="vertical" style={{ width: "100%" }}>
+              <Button
+                onClick={initializeSttManager}
+                disabled={!isSdkInitialized || isSttManagerInitialized}
+                block
+              >
+                {isSttManagerInitialized ? "✅ STT管理器已初始化" : "初始化STT管理器"}
+              </Button>
+
+              <Button
+                onClick={joinRtmChannel}
+                disabled={!isSdkInitialized || isRtmManagerJoined}
+                block
+              >
+                {isRtmManagerJoined ? "✅ RTM频道已加入" : "加入RTM频道"}
+              </Button>
+
+              <Divider />
+
+              <Button
+                type="primary"
+                icon={<PlayCircleOutlined />}
+                onClick={startTranscription}
+                disabled={!isSttManagerInitialized || isTranscriptionActive}
+                block
+              >
+                {isTranscriptionActive ? "转录进行中..." : "开始转录"}
+              </Button>
+
+              <Button
+                danger
+                icon={<StopOutlined />}
+                onClick={stopTranscription}
+                disabled={!isTranscriptionActive}
+                block
+              >
+                停止转录
+              </Button>
+
+              <Button
+                icon={<ReloadOutlined />}
+                onClick={queryTranscription}
+                disabled={!isTranscriptionActive}
+                block
+              >
+                查询状态
+              </Button>
+            </Space>
+
+            <Divider />
+
+            <Alert
+              message="转录状态"
+              description={
+                transcriptionStatus === "idle"
+                  ? "等待开始转录"
+                  : transcriptionStatus === "starting"
+                    ? "正在启动转录..."
+                    : transcriptionStatus === "active"
+                      ? "转录进行中"
+                      : transcriptionStatus === "stopping"
+                        ? "正在停止转录..."
+                        : transcriptionStatus === "stopped"
+                          ? "转录已停止"
+                          : "状态异常"
+              }
+              type={
+                transcriptionStatus === "active"
+                  ? "success"
+                  : transcriptionStatus === "error"
+                    ? "error"
+                    : "info"
+              }
+              showIcon
+            />
+          </Card>
+        </div>
+
+        <div className={styles.rightPanel}>
+          <Card title="📋 测试日志" className={styles.logCard}>
+            <div className={styles.logContainer}>
+              {testResults.length === 0 ? (
+                <Text type="secondary">暂无测试日志,请开始测试...</Text>
+              ) : (
+                testResults.map((log, index) => (
+                  <div key={index} className={styles.logEntry}>
+                    {log}
+                  </div>
+                ))
+              )}
+            </div>
+
+            {testResults.length > 0 && (
+              <Button onClick={() => setTestResults([])} size="small" style={{ marginTop: 16 }}>
+                清空日志
+              </Button>
+            )}
+          </Card>
+
+          <Card title="💡 使用说明" className={styles.infoCard}>
+            <Space direction="vertical" style={{ width: "100%" }}>
+              <Text type="secondary">
+                <InfoCircleOutlined /> 测试步骤:
+              </Text>
+              <ol>
+                <li>填写 App ID 和 Certificate</li>
+                <li>点击"初始化 SDK"</li>
+                <li>初始化 STT 管理器和加入 RTM 频道</li>
+                <li>开始/停止转录测试</li>
+                <li>查看测试日志了解详细结果</li>
+              </ol>
+
+              <Button type="link" onClick={goToMainApp}>
+                返回主应用
+              </Button>
+            </Space>
+          </Card>
+        </div>
+      </div>
+    </div>
+  )
+}
+
+export default SdkTestPage

+ 2 - 0
src/router/index.tsx

@@ -4,11 +4,13 @@ import { Route, createHashRouter, RouterProvider, createRoutesFromElements } fro
 const HomePage = lazy(() => import("../pages/home"))
 const LoginPage = lazy(() => import("../pages/login"))
 const NotFoundPage = lazy(() => import("../pages/404"))
+const SdkTestPage = lazy(() => import("../pages/sdk-test"))
 
 const routerItems = [
   <Route path="/" element={<LoginPage />} />,
   <Route path="/home" element={<HomePage />} />,
   <Route path="/login" element={<LoginPage />} />,
+  <Route path="/sdk-test" element={<SdkTestPage />} />,
   <Route path="*" element={<NotFoundPage />} />,
 ]
 

+ 151 - 0
tests/e2e/sdk-test.spec.ts

@@ -0,0 +1,151 @@
+import { test, expect } from "@playwright/test"
+
+test.describe("SDK Test Page E2E Tests", () => {
+  test.beforeEach(async ({ page }) => {
+    // 导航到SDK测试页面
+    await page.goto("/sdk-test")
+  })
+
+  test("should load SDK test page successfully", async ({ page }) => {
+    // 验证页面标题
+    await expect(page.locator("h2")).toContainText("SDK 功能测试页面")
+
+    // 验证配置表单存在
+    await expect(page.locator('input[placeholder="请输入 Agora App ID"]')).toBeVisible()
+    await expect(page.locator('input[placeholder="请输入 Agora Certificate"]')).toBeVisible()
+    await expect(page.locator('input[placeholder="请输入频道名称"]')).toBeVisible()
+    await expect(page.locator('input[placeholder="请输入用户名称"]')).toBeVisible()
+  })
+
+  test("should initialize SDK with valid credentials", async ({ page }) => {
+    // 填写配置信息
+    await page.fill('input[placeholder="请输入 Agora App ID"]', "test-app-id")
+    await page.fill('input[placeholder="请输入 Agora Certificate"]', "test-certificate")
+
+    // 点击初始化SDK按钮
+    await page.click('button:has-text("初始化 SDK")')
+
+    // 验证初始化成功
+    await expect(page.locator('button:has-text("✅ 已初始化")')).toBeVisible()
+
+    // 验证测试日志显示初始化成功
+    await expect(page.locator(".logContainer")).toContainText("✅ SDK初始化成功")
+  })
+
+  test("should show error message with invalid credentials", async ({ page }) => {
+    // 填写空的配置信息
+    await page.fill('input[placeholder="请输入 Agora App ID"]', "")
+    await page.fill('input[placeholder="请输入 Agora Certificate"]', "")
+
+    // 点击初始化SDK按钮
+    await page.click('button:has-text("初始化 SDK")')
+
+    // 验证错误消息显示
+    await expect(page.locator(".ant-message-error")).toBeVisible()
+  })
+
+  test("should initialize STT manager after SDK initialization", async ({ page }) => {
+    // 先初始化SDK
+    await page.fill('input[placeholder="请输入 Agora App ID"]', "test-app-id")
+    await page.fill('input[placeholder="请输入 Agora Certificate"]', "test-certificate")
+    await page.click('button:has-text("初始化 SDK")')
+
+    // 点击初始化STT管理器按钮
+    await page.click('button:has-text("初始化STT管理器")')
+
+    // 验证STT管理器初始化成功
+    await expect(page.locator('button:has-text("✅ STT管理器已初始化")')).toBeVisible()
+    await expect(page.locator(".logContainer")).toContainText("✅ STT管理器初始化成功")
+  })
+
+  test("should join RTM channel successfully", async ({ page }) => {
+    // 先初始化SDK
+    await page.fill('input[placeholder="请输入 Agora App ID"]', "test-app-id")
+    await page.fill('input[placeholder="请输入 Agora Certificate"]', "test-certificate")
+    await page.click('button:has-text("初始化 SDK")')
+
+    // 点击加入RTM频道按钮
+    await page.click('button:has-text("加入RTM频道")')
+
+    // 验证RTM频道加入成功
+    await expect(page.locator('button:has-text("✅ RTM频道已加入")')).toBeVisible()
+    await expect(page.locator(".logContainer")).toContainText("✅ RTM频道加入成功")
+  })
+
+  test("should start and stop transcription", async ({ page }) => {
+    // 先完成所有初始化步骤
+    await page.fill('input[placeholder="请输入 Agora App ID"]', "test-app-id")
+    await page.fill('input[placeholder="请输入 Agora Certificate"]', "test-certificate")
+    await page.click('button:has-text("初始化 SDK")')
+    await page.click('button:has-text("初始化STT管理器")')
+
+    // 点击开始转录按钮
+    await page.click('button:has-text("开始转录")')
+
+    // 验证转录状态
+    await expect(page.locator('button:has-text("转录进行中...")')).toBeVisible()
+    await expect(page.locator(".logContainer")).toContainText("✅ 语音转录已开始")
+
+    // 点击停止转录按钮
+    await page.click('button:has-text("停止转录")')
+
+    // 验证转录已停止
+    await expect(page.locator('button:has-text("开始转录")')).toBeVisible()
+    await expect(page.locator(".logContainer")).toContainText("✅ 语音转录已停止")
+  })
+
+  test("should query transcription status", async ({ page }) => {
+    // 先完成所有初始化步骤并开始转录
+    await page.fill('input[placeholder="请输入 Agora App ID"]', "test-app-id")
+    await page.fill('input[placeholder="请输入 Agora Certificate"]', "test-certificate")
+    await page.click('button:has-text("初始化 SDK")')
+    await page.click('button:has-text("初始化STT管理器")')
+    await page.click('button:has-text("开始转录")')
+
+    // 点击查询状态按钮
+    await page.click('button:has-text("查询状态")')
+
+    // 验证查询结果日志
+    await expect(page.locator(".logContainer")).toContainText("📊 转录状态查询结果")
+  })
+
+  test("should cleanup resources properly", async ({ page }) => {
+    // 先完成所有初始化步骤
+    await page.fill('input[placeholder="请输入 Agora App ID"]', "test-app-id")
+    await page.fill('input[placeholder="请输入 Agora Certificate"]', "test-certificate")
+    await page.click('button:has-text("初始化 SDK")')
+    await page.click('button:has-text("初始化STT管理器")')
+
+    // 点击清理资源按钮
+    await page.click('button:has-text("清理资源")')
+
+    // 验证资源清理成功
+    await expect(page.locator('button:has-text("初始化 SDK")')).toBeVisible()
+    await expect(page.locator(".logContainer")).toContainText("✅ 资源清理完成")
+  })
+
+  test("should navigate to main app from SDK test page", async ({ page }) => {
+    // 点击返回主应用链接
+    await page.click('button:has-text("返回主应用")')
+
+    // 验证导航到登录页面
+    await expect(page).toHaveURL(/.*\/login/)
+  })
+
+  test("should display test logs correctly", async ({ page }) => {
+    // 执行一些操作生成日志
+    await page.fill('input[placeholder="请输入 Agora App ID"]', "test-app-id")
+    await page.fill('input[placeholder="请输入 Agora Certificate"]', "test-certificate")
+    await page.click('button:has-text("初始化 SDK")')
+
+    // 验证日志容器中有内容
+    const logContainer = page.locator(".logContainer")
+    await expect(logContainer).not.toContainText("暂无测试日志")
+
+    // 点击清空日志按钮
+    await page.click('button:has-text("清空日志")')
+
+    // 验证日志被清空
+    await expect(logContainer).toContainText("暂无测试日志")
+  })
+})

+ 3 - 2
tsconfig.json

@@ -20,10 +20,11 @@
     "noUnusedParameters": false,
     "noFallthroughCasesInSwitch": true,
     "paths": {
-      "@/*": ["src/*"]
+      "@/*": ["src/*"],
+      "@/packages/stt-sdk-core": ["packages/stt-sdk-core/src"]
     }
   },
-  "include": ["src"],
+  "include": ["src", "packages/stt-sdk-core/src"],
   "references": [
     {
       "path": "./tsconfig.node.json"

+ 1 - 0
vite.config.ts

@@ -18,6 +18,7 @@ export default defineConfig(({ mode }) => {
     resolve: {
       alias: {
         "@": "/src",
+        "@/packages/stt-sdk-core": "/packages/stt-sdk-core/src",
       },
     },
     base: genBaseUrl(mode),