| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525 |
- import { TIMEOUTS } from '../../utils/timeouts';
- import { test, expect } from '../../utils/test-setup';
- import { uploadFileToField } from '@d8d/e2e-test-utils';
- /**
- * 文件上传工具 E2E 验证测试
- *
- * Story 3.3: 在 E2E 测试中验证文件上传工具
- *
- * 目标:验证 `uploadFileToField()` 函数在真实场景中的可用性和稳定性
- *
- * 测试场景:
- * 1. 基本文件上传:使用 testId 选择器上传照片
- * 2. 多文件上传:连续上传多张照片
- * 3. 完整表单场景:验证文件上传后的表单提交
- * 4. 错误处理:文件不存在等异常场景
- * 5. 不同文件类型:验证 JPG/PNG/WEBP 格式支持
- *
- * 重要发现:
- * FileSelector 组件使用 Dialog 模式,MinioUploader 在对话框内部渲染。
- * 因此需要先打开对话框才能访问 data-testid="photo-upload-${index}" 元素。
- */
- // 统一的文件上传配置
- const UPLOAD_OPTIONS = { fixturesDir: 'tests/fixtures' };
- /**
- * 生成随机的身份证号(用于避免数据库唯一性冲突)
- * @param timestamp 时间戳,用于确保唯一性
- * @param suffix 额外的后缀(可选)
- * @returns 18位身份证号
- */
- function generateRandomIdCard(timestamp: number, suffix = 0): string {
- // 地区代码(42开头表示湖北省)
- const areaCode = '42';
- // 出生年月日(1990年01月01日)
- const birthDate = '19900101';
- // 顺序码(使用时间戳的后8位 + 后缀确保唯一性)
- const sequence = String(timestamp).slice(-8).padStart(3, '0').slice(-3);
- const sequenceWithSuffix = String(parseInt(sequence) + suffix).padStart(3, '0');
- // 校验码(随机生成0-9或X)
- const checksum = Math.floor(Math.random() * 10);
- return `${areaCode}0101${birthDate}${sequenceWithSuffix}${checksum}`;
- }
- /**
- * 生成随机的残疾证号(用于避免数据库唯一性冲突)
- * @param timestamp 时间戳,用于确保唯一性
- * @param suffix 额外的后缀(可选)
- * @returns 残疾证号
- */
- function generateRandomDisabilityId(timestamp: number, suffix = 0): string {
- // 使用时间戳的后6位 + 后缀确保唯一性
- const randomPart = String(timestamp).slice(-6).padStart(4, '0').slice(-4);
- const withSuffix = String(parseInt(randomPart) + suffix).padStart(4, '0');
- return `CJZ${withSuffix}`;
- }
- test.describe.serial('文件上传工具 E2E 验证', () => {
- test.beforeEach(async ({ adminLoginPage, disabilityPersonPage }) => {
- // 以管理员身份登录后台
- await adminLoginPage.goto();
- await adminLoginPage.login('admin', 'admin123');
- await adminLoginPage.expectLoginSuccess();
- await disabilityPersonPage.goto();
- });
- /**
- * 场景 1: 基本文件上传验证
- *
- * 验证 uploadFileToField() 能成功上传单个文件
- * AC: #2, #3
- */
- test('应该成功上传单张照片', async ({ disabilityPersonPage, page }) => {
- const timestamp = Date.now();
- console.debug('\n========== 场景 1: 基本文件上传 ==========');
- // 1. 打开对话框并填写基本信息
- await disabilityPersonPage.openCreateDialog();
- await disabilityPersonPage.fillBasicForm({
- name: `文件上传测试_${timestamp}`,
- gender: '男',
- idCard: generateRandomIdCard(timestamp, 0),
- disabilityId: generateRandomDisabilityId(timestamp, 0),
- disabilityType: '视力残疾',
- disabilityLevel: '一级',
- phone: '13800138000',
- idAddress: '湖北省武汉市测试街道1号',
- province: '湖北省',
- city: '武汉市'
- });
- // 2. 滚动到照片区域并添加一张照片
- await disabilityPersonPage.scrollToSection('照片');
- // 查找并点击"添加照片"按钮
- const addPhotoButton = page.getByRole('button', { name: /添加照片/ }).or(page.getByRole('button', { name: /\+/ }));
- await addPhotoButton.first().click();
- // 3. 点击"选择或上传照片"按钮,打开 FileSelector 对话框
- // 这样 MinioUploader 组件才会被渲染,data-testid 才会存在
- const selectFileButton = page.getByRole('button', { name: '选择或上传照片' }).or(
- page.getByText('选择或上传照片')
- );
- await selectFileButton.first().click();
- // 验证 FileSelector 对话框已打开(使用条件等待而非固定超时)
- const fileSelectorDialog = page.getByTestId('file-selector-dialog');
- await expect(fileSelectorDialog).toBeVisible({ timeout: TIMEOUTS.DIALOG });
- console.debug(' ✓ FileSelector 对话框已打开');
- // 4. 使用 uploadFileToField 上传文件
- // 注意:PhotoUploadField → FileSelector → MinioUploader
- // testId 格式:photo-upload-${index}
- console.debug(' [上传] 使用 uploadFileToField 上传 sample-id-card.jpg...');
- await uploadFileToField(
- page,
- '[data-testid="photo-upload-0"]',
- 'images/sample-id-card.jpg',
- { ...UPLOAD_OPTIONS, timeout: TIMEOUTS.DIALOG }
- );
- console.debug(' ✓ 文件上传操作已执行');
- // 5. 等待上传完成(等待文件预览出现)
- const previewImage = page.locator('[data-testid="photo-upload-0"]').locator('img').or(
- page.locator('[data-testid="photo-upload-0"]').locator('[alt*="照片"]')
- );
- await expect(previewImage.first()).toBeVisible({ timeout: TIMEOUTS.ELEMENT_VISIBLE_SHORT }).catch(() => {
- // 如果预览未出现,至少等待上传处理完成
- console.debug(' ⚠️ 预览未立即显示,继续测试');
- });
- // 6. 关闭 FileSelector 对话框(点击取消)
- const cancelButton = fileSelectorDialog.getByRole('button', { name: '取消' });
- await cancelButton.click();
- // 等待对话框关闭
- await expect(fileSelectorDialog).toBeHidden({ timeout: TIMEOUTS.VERY_LONG }).catch(() => {
- // 如果对话框未立即关闭,继续测试
- console.debug(' ⚠️ 对话框可能需要手动关闭');
- });
- // 取消主对话框
- await disabilityPersonPage.cancelDialog();
- console.debug('✅ 场景 1 完成:基本文件上传验证成功\n');
- });
- /**
- * 场景 2: 多文件上传验证
- *
- * 验证连续上传多张文件
- * AC: #2
- */
- test('应该成功上传多张照片', async ({ disabilityPersonPage, page }) => {
- const timestamp = Date.now();
- console.debug('\n========== 场景 2: 多文件上传 ==========');
- // 1. 打开对话框并填写基本信息
- await disabilityPersonPage.openCreateDialog();
- await disabilityPersonPage.fillBasicForm({
- name: `多文件上传测试_${timestamp}`,
- gender: '女',
- idCard: generateRandomIdCard(timestamp, 1),
- disabilityId: generateRandomDisabilityId(timestamp, 1),
- disabilityType: '听力残疾',
- disabilityLevel: '二级',
- phone: '13800138001',
- idAddress: '湖北省武汉市测试街道2号',
- province: '湖北省',
- city: '武汉市'
- });
- // 2. 滚动到照片区域
- await disabilityPersonPage.scrollToSection('照片');
- // 3. 连续添加并上传三张照片
- const addPhotoButton = page.getByRole('button', { name: /添加照片/ }).or(page.getByRole('button', { name: /\+/ }));
- // 辅助函数:上传单张照片
- const uploadSinglePhoto = async (photoNumber: number, fileName: string) => {
- // 点击"添加照片"
- await addPhotoButton.first().click();
- // 打开 FileSelector 对话框
- // 注意:每添加一张照片,都会有新的 FileSelector 按钮
- // 我们需要找到当前最新的那个(最后一个)
- const selectFileButton = page.getByRole('button', { name: '选择或上传照片' });
- const count = await selectFileButton.count();
- await selectFileButton.nth(count - 1).click();
- // 验证对话框已打开
- const fileSelectorDialog = page.getByTestId('file-selector-dialog');
- await expect(fileSelectorDialog).toBeVisible({ timeout: TIMEOUTS.ELEMENT_VISIBLE_SHORT });
- // 上传文件
- // 注意:testId 是基于照片卡片的索引,不是基于上传次数
- // 第一张照片的 testId 是 photo-upload-0,第二张是 photo-upload-1,以此类推
- console.debug(` [上传 ${photoNumber}/3] 上传 ${fileName}...`);
- await uploadFileToField(
- page,
- `[data-testid="photo-upload-${photoNumber - 1}"]`,
- fileName,
- UPLOAD_OPTIONS
- );
- console.debug(` ✓ 文件上传操作已执行`);
- // 等待一小段时间确保上传处理完成
- await page.waitForTimeout(TIMEOUTS.MEDIUM);
- // 关闭 FileSelector 对话框(点击取消)
- await fileSelectorDialog.getByRole('button', { name: '取消' }).click();
- };
- // 第一张照片 - 身份证照片
- await uploadSinglePhoto(1, 'images/sample-id-card.jpg');
- // 第二张照片 - 残疾证照片
- await uploadSinglePhoto(2, 'images/sample-disability-card.jpg');
- // 第三张照片 - 身份证照片(重复)
- await uploadSinglePhoto(3, 'images/sample-id-card.jpg');
- // 4. 验证照片卡片已添加
- const photoCards = page.locator('h4').filter({ hasText: /^照片 \d+$/ });
- const count = await photoCards.count();
- console.debug(` 检测到 ${count} 个照片卡片`);
- // 验证至少有 3 个照片卡片
- expect(count).toBeGreaterThanOrEqual(3);
- // 取消对话框
- await disabilityPersonPage.cancelDialog();
- console.debug('✅ 场景 2 完成:多文件上传验证成功\n');
- });
- /**
- * 场景 3: 完整表单提交验证
- *
- * 验证文件上传后的表单能成功提交
- * AC: #2, #4
- */
- test('应该成功提交包含照片的表单', async ({ disabilityPersonPage, page }) => {
- const timestamp = Date.now();
- const personName = `表单提交测试_${timestamp}`;
- console.debug('\n========== 场景 3: 完整表单提交 ==========');
- // 1. 打开对话框并填写基本信息
- await disabilityPersonPage.openCreateDialog();
- await disabilityPersonPage.fillBasicForm({
- name: personName,
- gender: '男',
- idCard: generateRandomIdCard(timestamp, 2),
- disabilityId: generateRandomDisabilityId(timestamp, 2),
- disabilityType: '肢体残疾',
- disabilityLevel: '三级',
- phone: '13800138002',
- idAddress: '湖北省武汉市测试街道3号',
- province: '湖北省',
- city: '武汉市'
- });
- // 2. 上传照片
- await disabilityPersonPage.scrollToSection('照片');
- const addPhotoButton = page.getByRole('button', { name: /添加照片/ }).or(page.getByRole('button', { name: /\+/ }));
- await addPhotoButton.first().click();
- // 打开 FileSelector 对话框
- const selectFileButton = page.getByRole('button', { name: '选择或上传照片' }).or(
- page.getByText('选择或上传照片')
- );
- await selectFileButton.first().click();
- const fileSelectorDialog = page.getByTestId('file-selector-dialog');
- await expect(fileSelectorDialog).toBeVisible({ timeout: TIMEOUTS.ELEMENT_VISIBLE_SHORT });
- console.debug(' [上传] 上传 sample-id-card.jpg...');
- await uploadFileToField(page, '[data-testid="photo-upload-0"]', 'images/sample-id-card.jpg', UPLOAD_OPTIONS);
- console.debug(' ✓ 文件上传操作已执行');
- // 等待上传处理完成
- await page.waitForTimeout(TIMEOUTS.MEDIUM);
- // 关闭 FileSelector 对话框
- await fileSelectorDialog.getByRole('button', { name: '取消' }).click();
- // 3. 提交表单
- console.debug(' [提交] 提交表单...');
- const result = await disabilityPersonPage.submitForm();
- // 4. 验证提交成功
- console.debug(' 检查提交结果...');
- console.debug(` - 有错误提示: ${result.hasError}`);
- console.debug(` - 有成功提示: ${result.hasSuccess}`);
- if (result.errorMessage) {
- console.debug(` - 错误消息: ${result.errorMessage}`);
- }
- if (result.successMessage) {
- console.debug(` - 成功消息: ${result.successMessage}`);
- }
- // 期望提交成功(无错误提示)
- expect(result.hasSuccess, '表单应该成功提交').toBe(true);
- expect(result.hasError, '表单不应该有错误').toBe(false);
- // 5. 验证对话框关闭
- await disabilityPersonPage.waitForDialogClosed();
- console.debug(' ✓ 对话框已关闭');
- // 6. 验证数据保存
- console.debug(' [验证] 检查数据是否保存...');
- // 刷新页面
- await page.reload();
- await page.waitForLoadState('networkidle');
- await disabilityPersonPage.goto();
- // 搜索刚创建的残疾人
- await disabilityPersonPage.searchByName(personName);
- const personExists = await disabilityPersonPage.personExists(personName);
- console.debug(` 数据保存成功: ${personExists}`);
- expect(personExists, '残疾人数据应该保存成功').toBe(true);
- console.debug('✅ 场景 3 完成:完整表单提交验证成功\n');
- });
- /**
- * 场景 4: 错误处理验证
- *
- * 验证错误处理机制
- * AC: #2
- */
- test('应该正确处理文件不存在错误', async ({ disabilityPersonPage, page }) => {
- const timestamp = Date.now();
- console.debug('\n========== 场景 4: 错误处理验证 ==========');
- // 1. 打开对话框并填写基本信息
- await disabilityPersonPage.openCreateDialog();
- await disabilityPersonPage.fillBasicForm({
- name: `错误处理测试_${timestamp}`,
- gender: '男',
- idCard: generateRandomIdCard(timestamp, 3),
- disabilityId: generateRandomDisabilityId(timestamp, 3),
- disabilityType: '视力残疾',
- disabilityLevel: '一级',
- phone: '13800138003',
- idAddress: '湖北省武汉市测试街道4号',
- province: '湖北省',
- city: '武汉市'
- });
- // 2. 尝试上传不存在的文件
- await disabilityPersonPage.scrollToSection('照片');
- const addPhotoButton = page.getByRole('button', { name: /添加照片/ }).or(page.getByRole('button', { name: /\+/ }));
- await addPhotoButton.first().click();
- // 打开 FileSelector 对话框
- const selectFileButton = page.getByRole('button', { name: '选择或上传照片' }).or(
- page.getByText('选择或上传照片')
- );
- await selectFileButton.first().click();
- const fileSelectorDialog = page.getByTestId('file-selector-dialog');
- await expect(fileSelectorDialog).toBeVisible({ timeout: TIMEOUTS.ELEMENT_VISIBLE_SHORT });
- console.debug(' [测试] 尝试上传不存在的文件...');
- let errorOccurred = false;
- let errorMessage = '';
- try {
- await uploadFileToField(page, '[data-testid="photo-upload-0"]', 'images/non-existent-file.jpg', UPLOAD_OPTIONS);
- } catch (error) {
- errorOccurred = true;
- errorMessage = error instanceof Error ? error.message : String(error);
- console.debug(` ✓ 捕获到预期错误: ${errorMessage}`);
- }
- // 验证错误被正确抛出
- expect(errorOccurred, '应该抛出文件不存在错误').toBe(true);
- expect(errorMessage, '错误消息应包含"uploadFileToField failed"或"文件不存在"')
- .toMatch(/uploadFileToField failed|文件不存在/);
- // 取消对话框
- await disabilityPersonPage.cancelDialog();
- console.debug('✅ 场景 4 完成:错误处理验证成功\n');
- });
- /**
- * 场景 2.5: 多文件 API 验证(Story 3.5 新增)
- *
- * 验证 uploadFileToField() 的多文件上传 API(文件数组参数)
- * AC: #1, #2, #6 (部分完成)
- *
- * ⚠️ UI 架构限制说明:
- * - 当前残疾人管理页面每个照片槽使用独立的 <input type="file"> 元素
- - - 不存在 <input type="file" multiple> 场景
- * - 因此无法在当前 UI 中真实测试一次调用上传多个文件
- *
- * ✅ API 已实现验证:
- * - TypeScript 类型签名支持文件数组参数
- * - 单元测试已验证多文件上传逻辑(16 个测试通过)
- * - 向后兼容性保持(单文件调用仍然工作)
- *
- * 📝 后续工作:
- * - 当有页面使用 <input type="file" multiple> 时,添加完整 E2E 测试
- * - 或考虑修改残疾人页面架构支持多文件选择
- */
- test('应该支持多文件上传 API(向后兼容验证)', async () => {
- console.debug('\n========== 场景 2.5: 多文件上传 API 验证 ==========');
- // 1. 验证单文件 API 类型仍然可用(向后兼容)
- console.debug(' [验证] 单文件 API 类型...');
- const singleFile: string = 'images/sample-id-card.jpg';
- expect(typeof singleFile).toBe('string');
- // 2. 验证多文件 API 类型正确
- console.debug(' [验证] 多文件 API 类型...');
- const multipleFiles: string[] = [
- 'images/sample-id-card.jpg',
- 'images/sample-disability-card.jpg',
- 'images/sample-id-card.jpg'
- ];
- expect(Array.isArray(multipleFiles)).toBe(true);
- expect(multipleFiles).toHaveLength(3);
- // 3. 验证 fixtures 测试文件存在
- const fs = await import('node:fs');
- const path = await import('node:path');
- for (const file of multipleFiles) {
- const filePath = path.join('tests/fixtures', file);
- const exists = fs.existsSync(filePath);
- console.debug(` ${file}: ${exists ? '✓ 存在' : '✗ 不存在'}`);
- expect(exists, `测试文件 ${file} 应该存在`).toBe(true);
- }
- // 4. 验证 API 导出和类型检查
- console.debug(' [验证] API 导出正确性...');
- const { uploadFileToField } = await import('@d8d/e2e-test-utils');
- expect(typeof uploadFileToField).toBe('function');
- console.debug('✅ 场景 2.5 完成:多文件上传 API 验证成功\n');
- console.debug(' ℹ️ 说明: 完整的 E2E 多文件上传测试需要 UI 组件支持 <input type="file" multiple>');
- console.debug(' ℹ️ 当前残疾人页面使用独立 input 元素,因此无法进行真实的多文件上传 E2E 测试\n');
- });
- /**
- * 场景 5: 不同文件类型验证
- *
- * 验证不同图片格式的上传
- * AC: #2
- */
- test('应该支持不同格式的图片文件', async ({ disabilityPersonPage, page }) => {
- const timestamp = Date.now();
- console.debug('\n========== 场景 5: 不同文件类型 ==========');
- // 1. 打开对话框并填写基本信息
- await disabilityPersonPage.openCreateDialog();
- await disabilityPersonPage.fillBasicForm({
- name: `文件类型测试_${timestamp}`,
- gender: '女',
- idCard: generateRandomIdCard(timestamp, 4),
- disabilityId: generateRandomDisabilityId(timestamp, 4),
- disabilityType: '言语残疾',
- disabilityLevel: '四级',
- phone: '13800138004',
- idAddress: '湖北省武汉市测试街道5号',
- province: '湖北省',
- city: '武汉市'
- });
- // 2. 上传不同格式的图片
- await disabilityPersonPage.scrollToSection('照片');
- const addPhotoButton = page.getByRole('button', { name: /添加照片/ }).or(page.getByRole('button', { name: /\+/ }));
- // 支持的图片格式列表
- // 注意:当前 fixtures 目录只有 JPG 文件,PNG 和 WEBP 格式待添加
- const supportedFormats = [
- { name: 'JPG', file: 'images/sample-id-card.jpg' },
- // TODO: 添加 PNG 和 WEBP 格式测试文件
- // { name: 'PNG', file: 'images/sample-id-card.png' },
- // { name: 'WEBP', file: 'images/sample-id-card.webp' },
- ];
- // 上传每种格式
- for (const format of supportedFormats) {
- console.debug(` [上传] 测试 ${format.name} 格式...`);
- await addPhotoButton.first().click();
- // 打开 FileSelector 对话框
- const selectFileButton = page.getByRole('button', { name: '选择或上传照片' }).or(
- page.getByText('选择或上传照片')
- );
- await selectFileButton.first().click();
- const fileSelectorDialog = page.getByTestId('file-selector-dialog');
- await expect(fileSelectorDialog).toBeVisible({ timeout: TIMEOUTS.ELEMENT_VISIBLE_SHORT });
- await uploadFileToField(page, '[data-testid="photo-upload-0"]', format.file, UPLOAD_OPTIONS);
- console.debug(` ✓ ${format.name} 格式图片上传操作已执行`);
- // 等待上传处理完成
- await page.waitForTimeout(TIMEOUTS.MEDIUM);
- // 关闭对话框
- await fileSelectorDialog.getByRole('button', { name: '取消' }).click();
- }
- console.debug(` 已验证 ${supportedFormats.length} 种图片格式`);
- console.debug(' 注意: PNG 和 WEBP 格式测试文件待添加');
- // 取消对话框
- await disabilityPersonPage.cancelDialog();
- console.debug('✅ 场景 5 完成:不同文件类型验证成功\n');
- });
- });
|