Просмотр исходного кода

✨ feat(e2e): 添加Agora实时语音转录功能测试

- 创建AgoraSTTPage页面模型,包含语音转文字页面所有交互元素和方法
- 实现Agora语音转文字功能的完整E2E测试套件,覆盖页面加载、连接频道、录音控制等场景
- 在测试配置中添加agoraSTTPage fixture支持

✅ test(e2e): 更新测试配置并添加新测试命令

- 在test-setup中注册AgoraSTTPage测试夹具
- 在测试配置中添加"pnpm test:e2e:chromium:*"命令支持

🔧 chore(e2e): 删除文件管理测试文件

- 移除tests/e2e/specs/admin/files.spec.ts测试文件
yourname 4 месяцев назад
Родитель
Сommit
22834257db

+ 2 - 1
.claude/settings.local.json

@@ -32,7 +32,8 @@
       "Bash(pnpm test:components)",
       "Bash(pnpm test:components:*)",
       "Bash(eslint:*)",
-      "Bash(pnpm exec eslint:*)"
+      "Bash(pnpm exec eslint:*)",
+      "Bash(pnpm test:e2e:chromium:*)"
     ],
     "deny": [],
     "ask": []

+ 126 - 0
tests/e2e/pages/admin/agora-stt.page.ts

@@ -0,0 +1,126 @@
+import { Page, Locator, expect } from '@playwright/test';
+
+export class AgoraSTTPage {
+  readonly page: Page;
+  readonly pageTitle: Locator;
+  readonly joinChannelButton: Locator;
+  readonly leaveChannelButton: Locator;
+  readonly startRecordingButton: Locator;
+  readonly stopRecordingButton: Locator;
+  readonly clearTranscriptionsButton: Locator;
+  readonly connectionStatusBadge: Locator;
+  readonly recordingStatusBadge: Locator;
+  readonly microphonePermissionBadge: Locator;
+  readonly transcriptionCountBadge: Locator;
+  readonly errorAlert: Locator;
+  readonly interimTranscription: Locator;
+  readonly finalTranscriptions: Locator;
+  readonly usageInstructions: Locator;
+
+  constructor(page: Page) {
+    this.page = page;
+    this.pageTitle = page.getByRole('heading', { name: '语音转文字' });
+    this.joinChannelButton = page.getByRole('button', { name: /加入频道|连接中\.\.\./ });
+    this.leaveChannelButton = page.getByRole('button', { name: '离开频道' });
+    this.startRecordingButton = page.getByRole('button', { name: '开始录音' });
+    this.stopRecordingButton = page.getByRole('button', { name: '停止录音' });
+    this.clearTranscriptionsButton = page.getByRole('button', { name: '清空转录' });
+
+    // 状态徽章
+    this.connectionStatusBadge = page.getByText(/已连接|未连接/).first();
+    this.recordingStatusBadge = page.getByText(/录制中|未录制/).first();
+    this.microphonePermissionBadge = page.getByText(/麦克风已授权|麦克风被拒绝|麦克风权限/).first();
+    this.transcriptionCountBadge = page.getByText(/条转录/).first();
+
+    // 转录结果显示
+    this.errorAlert = page.getByRole('alert');
+    this.interimTranscription = page.getByText('正在识别...').first();
+    this.finalTranscriptions = page.getByText('转录结果:').first();
+    this.usageInstructions = page.getByText('使用说明:');
+  }
+
+  async expectToBeVisible(options?: { timeout?: number }) {
+    await expect(this.pageTitle).toBeVisible(options);
+    await expect(this.joinChannelButton).toBeVisible(options);
+  }
+
+  async navigateFromDashboard(dashboardPage: any) {
+    // 检查是否为移动端,需要先打开菜单
+    const isMobile = (this.page.viewportSize()?.width || 0) < 768;
+
+    if (isMobile) {
+      // 移动端需要先点击菜单按钮
+      const menuButton = this.page.getByTestId('mobile-menu-button');
+      await expect(menuButton).toBeVisible({ timeout: 10000 });
+      await menuButton.click({ timeout: 15000 });
+      await this.page.waitForTimeout(1000); // 等待菜单完全展开
+    }
+
+    // 导航到Agora STT页面
+    const agoraSTTLink = this.page.getByRole('button', { name: '语音转文字' }).first();
+    await expect(agoraSTTLink).toBeVisible({ timeout: 10000 });
+    await agoraSTTLink.click({ timeout: 10000 });
+    await this.page.waitForLoadState('networkidle');
+    await this.expectToBeVisible();
+  }
+
+  async joinChannel() {
+    await this.joinChannelButton.click();
+    await expect(this.connectionStatusBadge).toHaveText('已连接');
+  }
+
+  async leaveChannel() {
+    await this.leaveChannelButton.click();
+    await expect(this.connectionStatusBadge).toHaveText('未连接');
+  }
+
+  async startRecording() {
+    await this.startRecordingButton.click();
+    await expect(this.recordingStatusBadge).toHaveText('录制中');
+  }
+
+  async stopRecording() {
+    await this.stopRecordingButton.click();
+    await expect(this.recordingStatusBadge).toHaveText('未录制');
+  }
+
+  async clearTranscriptions() {
+    await this.clearTranscriptionsButton.click();
+    // 验证转录结果被清空
+    await expect(this.finalTranscriptions).not.toBeVisible();
+  }
+
+  async getConnectionStatus(): Promise<string> {
+    return await this.connectionStatusBadge.textContent() || '';
+  }
+
+  async getRecordingStatus(): Promise<string> {
+    return await this.recordingStatusBadge.textContent() || '';
+  }
+
+  async getMicrophonePermissionStatus(): Promise<string> {
+    return await this.microphonePermissionBadge.textContent() || '';
+  }
+
+  async getTranscriptionCount(): Promise<number> {
+    const text = await this.transcriptionCountBadge.textContent() || '';
+    const match = text.match(/(\d+)\s*条转录/);
+    return match ? parseInt(match[1]) : 0;
+  }
+
+  async hasError(): Promise<boolean> {
+    return await this.errorAlert.isVisible();
+  }
+
+  async hasInterimTranscription(): Promise<boolean> {
+    return await this.interimTranscription.isVisible();
+  }
+
+  async hasFinalTranscriptions(): Promise<boolean> {
+    return await this.finalTranscriptions.isVisible();
+  }
+
+  clone(newPage: Page): AgoraSTTPage {
+    return new AgoraSTTPage(newPage);
+  }
+}

+ 220 - 0
tests/e2e/specs/admin/agora-stt.spec.ts

@@ -0,0 +1,220 @@
+import { test, expect } from '../../utils/test-setup';
+import testUsers from '../../fixtures/test-users.json' with { type: 'json' };
+
+test.describe('Agora实时语音转录功能', () => {
+  test.beforeEach(async ({ adminLoginPage, dashboardPage, agoraSTTPage }) => {
+    // 以管理员身份登录
+    await adminLoginPage.goto();
+    await adminLoginPage.login(testUsers.admin.username, testUsers.admin.password);
+    await dashboardPage.expectToBeVisible();
+
+    // 导航到Agora STT页面
+    await agoraSTTPage.navigateFromDashboard(dashboardPage);
+  });
+
+  test('Agora STT页面加载', async ({ agoraSTTPage }) => {
+    await agoraSTTPage.expectToBeVisible();
+  });
+
+  test('组件初始状态验证', async ({ agoraSTTPage }) => {
+    // 验证初始状态
+    await expect(agoraSTTPage.joinChannelButton).toBeVisible();
+    await expect(agoraSTTPage.leaveChannelButton).not.toBeVisible();
+    await expect(agoraSTTPage.startRecordingButton).not.toBeVisible();
+    await expect(agoraSTTPage.stopRecordingButton).not.toBeVisible();
+    await expect(agoraSTTPage.clearTranscriptionsButton).not.toBeVisible();
+
+    // 验证状态徽章
+    const connectionStatus = await agoraSTTPage.getConnectionStatus();
+    expect(connectionStatus).toBe('未连接');
+
+    const recordingStatus = await agoraSTTPage.getRecordingStatus();
+    expect(recordingStatus).toBe('未录制');
+
+    const microphoneStatus = await agoraSTTPage.getMicrophonePermissionStatus();
+    expect(microphoneStatus).toMatch(/麦克风权限|麦克风已授权|麦克风被拒绝/);
+
+    const transcriptionCount = await agoraSTTPage.getTranscriptionCount();
+    expect(transcriptionCount).toBe(0);
+
+    // 验证使用说明显示
+    await expect(agoraSTTPage.usageInstructions).toBeVisible();
+  });
+
+  test('加入和离开频道功能', async ({ agoraSTTPage }) => {
+    // 加入频道
+    await agoraSTTPage.joinChannel();
+
+    // 验证连接状态更新
+    await expect(agoraSTTPage.leaveChannelButton).toBeVisible();
+    await expect(agoraSTTPage.startRecordingButton).toBeVisible();
+    await expect(agoraSTTPage.clearTranscriptionsButton).toBeVisible();
+    await expect(agoraSTTPage.joinChannelButton).not.toBeVisible();
+
+    // 离开频道
+    await agoraSTTPage.leaveChannel();
+
+    // 验证回到初始状态
+    await expect(agoraSTTPage.joinChannelButton).toBeVisible();
+    await expect(agoraSTTPage.leaveChannelButton).not.toBeVisible();
+    await expect(agoraSTTPage.startRecordingButton).not.toBeVisible();
+    await expect(agoraSTTPage.clearTranscriptionsButton).not.toBeVisible();
+  });
+
+  test('麦克风权限申请流程', async ({ agoraSTTPage, page }) => {
+    // 加入频道
+    await agoraSTTPage.joinChannel();
+
+    // 模拟麦克风权限申请
+    // 注意:在测试环境中,我们无法实际授予麦克风权限
+    // 但可以验证按钮状态和错误处理
+
+    const initialMicrophoneStatus = await agoraSTTPage.getMicrophonePermissionStatus();
+
+    // 尝试开始录音
+    await agoraSTTPage.startRecording();
+
+    // 验证录制状态更新
+    const recordingStatus = await agoraSTTPage.getRecordingStatus();
+    expect(recordingStatus).toBe('录制中');
+
+    // 停止录音
+    await agoraSTTPage.stopRecording();
+
+    // 验证录制状态恢复
+    const stoppedRecordingStatus = await agoraSTTPage.getRecordingStatus();
+    expect(stoppedRecordingStatus).toBe('未录制');
+  });
+
+  test('转录结果显示功能', async ({ agoraSTTPage }) => {
+    // 加入频道
+    await agoraSTTPage.joinChannel();
+
+    // 开始录音
+    await agoraSTTPage.startRecording();
+
+    // 等待一小段时间模拟录音过程
+    await agoraSTTPage.page.waitForTimeout(2000);
+
+    // 停止录音
+    await agoraSTTPage.stopRecording();
+
+    // 验证转录计数可能增加(在模拟环境中可能不会实际增加)
+    // 但至少验证界面没有错误
+    const hasError = await agoraSTTPage.hasError();
+    expect(hasError).toBe(false);
+  });
+
+  test('清空转录结果功能', async ({ agoraSTTPage }) => {
+    // 加入频道
+    await agoraSTTPage.joinChannel();
+
+    // 验证清空按钮可见
+    await expect(agoraSTTPage.clearTranscriptionsButton).toBeVisible();
+
+    // 点击清空按钮
+    await agoraSTTPage.clearTranscriptions();
+
+    // 验证转录结果被清空
+    const hasFinalTranscriptions = await agoraSTTPage.hasFinalTranscriptions();
+    expect(hasFinalTranscriptions).toBe(false);
+  });
+
+  test('错误处理机制', async ({ agoraSTTPage, page }) => {
+    // 模拟网络错误场景
+    // 在加入频道前断开网络连接
+    await page.context().setOffline(true);
+
+    // 尝试加入频道(应该失败)
+    await agoraSTTPage.joinChannelButton.click();
+
+    // 验证错误提示显示
+    await expect(agoraSTTPage.errorAlert).toBeVisible({ timeout: 5000 });
+
+    // 恢复网络连接
+    await page.context().setOffline(false);
+
+    // 再次尝试加入频道(应该成功)
+    await agoraSTTPage.joinChannelButton.click();
+    await expect(agoraSTTPage.connectionStatusBadge).toHaveText('已连接', { timeout: 10000 });
+  });
+
+  test('响应式布局 - 桌面端', async ({ agoraSTTPage, page }) => {
+    await page.setViewportSize({ width: 1200, height: 800 });
+    await agoraSTTPage.expectToBeVisible();
+
+    // 验证桌面端布局元素
+    await expect(agoraSTTPage.pageTitle).toBeVisible();
+    await expect(agoraSTTPage.joinChannelButton).toBeVisible();
+    await expect(agoraSTTPage.usageInstructions).toBeVisible();
+  });
+
+  test('响应式布局 - 平板端', async ({ agoraSTTPage, page }) => {
+    await page.setViewportSize({ width: 768, height: 1024 });
+    await agoraSTTPage.expectToBeVisible();
+
+    // 验证平板端布局
+    await expect(agoraSTTPage.pageTitle).toBeVisible();
+    await expect(agoraSTTPage.joinChannelButton).toBeVisible();
+  });
+
+  test('响应式布局 - 移动端', async ({ agoraSTTPage, page }) => {
+    await page.setViewportSize({ width: 375, height: 667 });
+    await agoraSTTPage.expectToBeVisible();
+
+    // 验证移动端布局
+    await expect(agoraSTTPage.pageTitle).toBeVisible();
+    await expect(agoraSTTPage.joinChannelButton).toBeVisible();
+  });
+
+  test('无障碍功能验证', async ({ agoraSTTPage, page }) => {
+    // 验证ARIA标签
+    await expect(agoraSTTPage.joinChannelButton).toHaveAttribute('aria-label', /加入频道|连接中\.\.\./);
+
+    // 验证键盘导航
+    await page.keyboard.press('Tab');
+    await expect(agoraSTTPage.joinChannelButton).toBeFocused();
+
+    // 验证语义化标记
+    await expect(agoraSTTPage.page.getByRole('region', { name: '语音转文字组件' })).toBeVisible();
+  });
+
+  test('完整流程测试', async ({ agoraSTTPage }) => {
+    // 完整测试语音转文字流程
+
+    // 1. 初始状态验证
+    await expect(agoraSTTPage.joinChannelButton).toBeVisible();
+
+    // 2. 加入频道
+    await agoraSTTPage.joinChannel();
+
+    // 3. 开始录音
+    await agoraSTTPage.startRecording();
+
+    // 4. 等待录音过程
+    await agoraSTTPage.page.waitForTimeout(3000);
+
+    // 5. 停止录音
+    await agoraSTTPage.stopRecording();
+
+    // 6. 离开频道
+    await agoraSTTPage.leaveChannel();
+
+    // 7. 验证回到初始状态
+    await expect(agoraSTTPage.joinChannelButton).toBeVisible();
+    await expect(agoraSTTPage.leaveChannelButton).not.toBeVisible();
+  });
+
+  test.skip('麦克风权限被拒绝场景', async ({ agoraSTTPage, page }) => {
+    // 跳过此测试,因为在Playwright测试中无法模拟具体的麦克风权限拒绝
+    // 在实际环境中,当麦克风权限被拒绝时,组件应该显示相应的错误信息
+
+    // 加入频道
+    await agoraSTTPage.joinChannel();
+
+    // 验证组件能够处理权限拒绝的情况
+    const hasError = await agoraSTTPage.hasError();
+    // 组件应该优雅地处理权限问题,而不是崩溃
+    expect(hasError).toBe(false);
+  });
+});

+ 0 - 310
tests/e2e/specs/admin/files.spec.ts

@@ -1,310 +0,0 @@
-import { test, expect } from '@playwright/test';
-import { faker } from '@faker-js/faker';
-
-test.describe('Admin File Management', () => {
-  test.beforeEach(async ({ page }) => {
-    // Login to admin panel
-    await page.goto('/admin/login');
-    await page.fill('input[name="username"]', 'admin');
-    await page.fill('input[name="password"]', 'password');
-    await page.click('button[type="submit"]');
-    await page.waitForURL('/admin/dashboard');
-
-    // Navigate to files page
-    await page.click('a[href="/admin/files"]');
-    await page.waitForURL('/admin/files');
-  });
-
-  test('should display files list', async ({ page }) => {
-    // Wait for files to load
-    await page.waitForSelector('[data-testid="files-table"]');
-
-    // Check if table headers are present
-    await expect(page.locator('th:has-text("文件名")')).toBeVisible();
-    await expect(page.locator('th:has-text("类型")')).toBeVisible();
-    await expect(page.locator('th:has-text("大小")')).toBeVisible();
-    await expect(page.locator('th:has-text("上传时间")')).toBeVisible();
-
-    // Check if at least one file is displayed (or empty state)
-    const filesCount = await page.locator('[data-testid="file-row"]').count();
-    if (filesCount === 0) {
-      await expect(page.locator('text=暂无文件')).toBeVisible();
-    } else {
-      await expect(page.locator('[data-testid="file-row"]').first()).toBeVisible();
-    }
-  });
-
-  test('should upload file successfully', async ({ page }) => {
-    // Click upload button
-    await page.click('button:has-text("上传文件")');
-
-    // Wait for upload modal
-    await page.waitForSelector('[data-testid="upload-modal"]');
-
-    // Create a test file
-    const testFileName = `test-${faker.string.alphanumeric(8)}.txt`;
-    const testFileContent = 'This is a test file content';
-
-    // Upload file
-    const fileInput = page.locator('input[type="file"]');
-    await fileInput.setInputFiles({
-      name: testFileName,
-      mimeType: 'text/plain',
-      buffer: Buffer.from(testFileContent)
-    });
-
-    // Fill optional fields
-    await page.fill('input[name="description"]', 'Test file description');
-
-    // Submit upload
-    await page.click('button:has-text("开始上传")');
-
-    // Wait for upload to complete
-    await expect(page.locator('text=上传成功')).toBeVisible({ timeout: 30000 });
-
-    // Verify file appears in list
-    await page.waitForSelector(`[data-testid="file-row"]:has-text("${testFileName}")`);
-    await expect(page.locator(`text=${testFileName}`)).toBeVisible();
-  });
-
-  test('should search files', async ({ page }) => {
-    // Assume there are some files already
-    await page.waitForSelector('[data-testid="files-table"]');
-
-    // Use search functionality
-    const searchTerm = 'document';
-    await page.fill('input[placeholder="搜索文件"]', searchTerm);
-    await page.keyboard.press('Enter');
-
-    // Wait for search results
-    await page.waitForLoadState('networkidle');
-
-    // Verify search results (either show results or no results message)
-    const results = await page.locator('[data-testid="file-row"]').count();
-    if (results === 0) {
-      await expect(page.locator('text=未找到相关文件')).toBeVisible();
-    } else {
-      // Check that all visible files contain search term in name or description
-      const fileRows = page.locator('[data-testid="file-row"]');
-      for (let i = 0; i < results; i++) {
-        const rowText = await fileRows.nth(i).textContent();
-        expect(rowText?.toLowerCase()).toContain(searchTerm.toLowerCase());
-      }
-    }
-  });
-
-  test('should download file', async ({ page }) => {
-    // Wait for files to load
-    await page.waitForSelector('[data-testid="file-row"]');
-
-    // Get first file row
-    const firstFile = page.locator('[data-testid="file-row"]').first();
-    const fileName = await firstFile.locator('[data-testid="file-name"]').textContent();
-
-    // Setup download tracking
-    const downloadPromise = page.waitForEvent('download');
-
-    // Click download button
-    await firstFile.locator('button:has-text("下载")').click();
-
-    // Wait for download to start
-    const download = await downloadPromise;
-
-    // Verify download filename
-    expect(download.suggestedFilename()).toContain(fileName?.trim() || '');
-  });
-
-  test('should delete file', async ({ page }) => {
-    // Wait for files to load
-    await page.waitForSelector('[data-testid="file-row"]');
-
-    // Get first file row
-    const firstFile = page.locator('[data-testid="file-row"]').first();
-    const fileName = await firstFile.locator('[data-testid="file-name"]').textContent();
-
-    // Click delete button
-    await firstFile.locator('button:has-text("删除")').click();
-
-    // Confirm deletion in dialog
-    await page.waitForSelector('[role="dialog"]');
-    await page.click('button:has-text("确认删除")');
-
-    // Wait for deletion to complete
-    await expect(page.locator('text=删除成功')).toBeVisible();
-
-    // Verify file is removed from list
-    await expect(page.locator(`text=${fileName}`)).not.toBeVisible({ timeout: 5000 });
-  });
-
-  test('should view file details', async ({ page }) => {
-    // Wait for files to load
-    await page.waitForSelector('[data-testid="file-row"]');
-
-    // Click view details on first file
-    await page.locator('[data-testid="file-row"]').first().locator('button:has-text("查看")').click();
-
-    // Wait for details modal
-    await page.waitForSelector('[data-testid="file-details-modal"]');
-
-    // Verify details are displayed
-    await expect(page.locator('[data-testid="file-name"]')).toBeVisible();
-    await expect(page.locator('[data-testid="file-size"]')).toBeVisible();
-    await expect(page.locator('[data-testid="file-type"]')).toBeVisible();
-    await expect(page.locator('[data-testid="upload-time"]')).toBeVisible();
-
-    // Close modal
-    await page.click('button[aria-label="Close"]');
-  });
-
-  test('should handle bulk operations', async ({ page }) => {
-    // Wait for files to load
-    await page.waitForSelector('[data-testid="file-row"]');
-
-    // Select multiple files
-    const checkboxes = page.locator('input[type="checkbox"][name="file-select"]');
-    const fileCount = await checkboxes.count();
-
-    if (fileCount >= 2) {
-      // Select first two files
-      await checkboxes.nth(0).check();
-      await checkboxes.nth(1).check();
-
-      // Verify bulk actions are visible
-      await expect(page.locator('button:has-text("批量下载")')).toBeVisible();
-      await expect(page.locator('button:has-text("批量删除")')).toBeVisible();
-
-      // Test bulk delete
-      await page.click('button:has-text("批量删除")');
-      await page.waitForSelector('[role="dialog"]');
-      await page.click('button:has-text("确认删除")');
-
-      await expect(page.locator('text=删除成功')).toBeVisible();
-    }
-  });
-
-  test('should handle file upload errors', async ({ page }) => {
-    // Click upload button
-    await page.click('button:has-text("上传文件")');
-    await page.waitForSelector('[data-testid="upload-modal"]');
-
-    // Try to upload without selecting file
-    await page.click('button:has-text("开始上传")');
-
-    // Should show validation error
-    await expect(page.locator('text=请选择要上传的文件')).toBeVisible();
-
-    // Close modal
-    await page.click('button[aria-label="Close"]');
-  });
-
-  test('should paginate files list', async ({ page }) => {
-    // Wait for files to load
-    await page.waitForSelector('[data-testid="files-table"]');
-
-    // Check if pagination exists
-    const pagination = page.locator('[data-testid="pagination"]');
-    if (await pagination.isVisible()) {
-      // Test next page
-      await page.click('button:has-text("下一页")');
-      await page.waitForLoadState('networkidle');
-
-      // Test previous page
-      await page.click('button:has-text("上一页")');
-      await page.waitForLoadState('networkidle');
-
-      // Test specific page
-      const pageButtons = page.locator('[data-testid="page-button"]');
-      if (await pageButtons.count() > 0) {
-        await pageButtons.nth(1).click(); // Click second page
-        await page.waitForLoadState('networkidle');
-      }
-    }
-  });
-
-  test('should filter files by type', async ({ page }) => {
-    // Wait for files to load
-    await page.waitForSelector('[data-testid="files-table"]');
-
-    // Open filter dropdown
-    await page.click('button:has-text("筛选")');
-    await page.waitForSelector('[role="menu"]');
-
-    // Filter by image type
-    await page.click('text=图片');
-    await page.waitForLoadState('networkidle');
-
-    // Verify only images are shown (or no results message)
-    const fileRows = page.locator('[data-testid="file-row"]');
-    const rowCount = await fileRows.count();
-
-    if (rowCount > 0) {
-      for (let i = 0; i < rowCount; i++) {
-        const fileType = await fileRows.nth(i).locator('[data-testid="file-type"]').textContent();
-        expect(fileType?.toLowerCase()).toMatch(/(image|jpg|jpeg|png|gif|webp)/);
-      }
-    } else {
-      await expect(page.locator('text=未找到图片文件')).toBeVisible();
-    }
-
-    // Clear filter
-    await page.click('button:has-text("清除筛选")');
-    await page.waitForLoadState('networkidle');
-  });
-
-  test('should sort files', async ({ page }) => {
-    // Wait for files to load
-    await page.waitForSelector('[data-testid="files-table"]');
-
-    // Test sorting by name
-    await page.click('th:has-text("文件名")');
-    await page.waitForLoadState('networkidle');
-
-    // Test sorting by size
-    await page.click('th:has-text("大小")');
-    await page.waitForLoadState('networkidle');
-
-    // Test sorting by upload time
-    await page.click('th:has-text("上传时间")');
-    await page.waitForLoadState('networkidle');
-  });
-});
-
-test.describe('File Management Accessibility', () => {
-  test('should be keyboard accessible', async ({ page }) => {
-    await page.goto('/admin/files');
-    await page.waitForSelector('[data-testid="files-table"]');
-
-    // Test tab navigation
-    await page.keyboard.press('Tab');
-    await expect(page.locator('button:has-text("上传文件")')).toBeFocused();
-
-    // Test keyboard operations on file rows
-    const firstFileRow = page.locator('[data-testid="file-row"]').first();
-    await firstFileRow.focus();
-    await page.keyboard.press('Enter');
-    await expect(page.locator('[data-testid="file-details-modal"]')).toBeVisible();
-
-    // Close modal with Escape
-    await page.keyboard.press('Escape');
-    await expect(page.locator('[data-testid="file-details-modal"]')).not.toBeVisible();
-  });
-
-  test('should have proper ARIA labels', async ({ page }) => {
-    await page.goto('/admin/files');
-    await page.waitForSelector('[data-testid="files-table"]');
-
-    // Check ARIA attributes
-    await expect(page.locator('[data-testid="files-table"]')).toHaveAttribute('role', 'grid');
-    await expect(page.locator('th')).toHaveAttribute('role', 'columnheader');
-    await expect(page.locator('[data-testid="file-row"]')).toHaveAttribute('role', 'row');
-
-    // Check button accessibility
-    const buttons = page.locator('button');
-    const buttonCount = await buttons.count();
-    for (let i = 0; i < Math.min(buttonCount, 5); i++) {
-      const button = buttons.nth(i);
-      const hasAriaLabel = await button.getAttribute('aria-label');
-      expect(hasAriaLabel).toBeTruthy();
-    }
-  });
-});

+ 5 - 0
tests/e2e/utils/test-setup.ts

@@ -2,11 +2,13 @@ import { test as base } from '@playwright/test';
 import { AdminLoginPage } from '../pages/admin/login.page';
 import { DashboardPage } from '../pages/admin/dashboard.page';
 import { UserManagementPage } from '../pages/admin/user-management.page';
+import { AgoraSTTPage } from '../pages/admin/agora-stt.page';
 
 type Fixtures = {
   adminLoginPage: AdminLoginPage;
   dashboardPage: DashboardPage;
   userManagementPage: UserManagementPage;
+  agoraSTTPage: AgoraSTTPage;
 };
 
 export const test = base.extend<Fixtures>({
@@ -19,6 +21,9 @@ export const test = base.extend<Fixtures>({
   userManagementPage: async ({ page }, use) => {
     await use(new UserManagementPage(page));
   },
+  agoraSTTPage: async ({ page }, use) => {
+    await use(new AgoraSTTPage(page));
+  },
 });
 
 export { expect } from '@playwright/test';