| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150 |
- import * as fs from 'node:fs';
- import * as path from 'node:path';
- import type { Page } from '@playwright/test';
- import type { FileUploadOptions } from './types';
- import { throwError } from './errors';
- import { DEFAULT_TIMEOUTS } from './constants';
- /**
- * 上传文件到指定输入框
- *
- * @description
- * 从 fixtures 目录加载测试文件并上传到指定的文件输入框。
- * 自动处理文件路径解析、存在性检查和错误处理。
- *
- * 支持的选择器策略:
- * - `data-testid` - 推荐,最稳定
- * - `aria-label` - 无障碍属性
- * - CSS 选择器 - 直接使用
- *
- * @param page - Playwright Page 对象
- * @param selector - 文件输入框的选择器
- * @param fileName - 要上传的文件名(相对于 fixtures 目录)
- * @param options - 可选配置
- * @param options.fixturesDir - fixtures 目录路径,默认为 'web/tests/fixtures'
- * @param options.timeout - 超时时间(毫秒),默认 5000ms
- * @param options.waitForUpload - 是否等待上传完成,默认为 true
- * @throws {E2ETestError} 当文件不存在或选择器无效时
- *
- * @example
- * ```ts
- * // 默认配置上传
- * await uploadFileToField(page, 'photo-upload', 'sample-id-card.jpg');
- *
- * // 自定义 fixtures 目录
- * await uploadFileToField(page, 'photo-upload', 'sample-id-card.jpg', {
- * fixturesDir: 'custom/fixtures/path'
- * });
- *
- * // 支持子目录
- * await uploadFileToField(page, 'document-upload', 'docs/resume.pdf', {
- * fixturesDir: 'tests/fixtures'
- * });
- * ```
- */
- export async function uploadFileToField(
- page: Page,
- selector: string,
- fileName: string,
- options?: FileUploadOptions
- ): Promise<void> {
- console.debug(`[uploadFileToField] 开始上传: selector="${selector}", fileName="${fileName}"`);
- // 1. 合并默认配置
- const config = {
- timeout: options?.timeout ?? DEFAULT_TIMEOUTS.static,
- fixturesDir: options?.fixturesDir ?? 'web/tests/fixtures',
- waitForUpload: options?.waitForUpload ?? true
- };
- // 2. 解析文件路径
- const filePath = resolveFixturePath(fileName, config.fixturesDir);
- console.debug(`[uploadFileToField] 解析文件路径: ${filePath}`);
- // 3. 检查文件是否存在
- if (!fs.existsSync(filePath)) {
- throwError({
- operation: 'uploadFileToField',
- target: `文件 "${fileName}"`,
- expected: `文件存在于 ${filePath}`,
- suggestion: '检查文件名是否正确,或确认文件已添加到 fixtures 目录'
- });
- }
- // 4. 查找文件输入框并上传
- try {
- const fileInput = page.locator(selector);
- console.debug(`[uploadFileToField] 找到文件输入框,准备上传`);
- // 使用 setInputFiles API
- await fileInput.setInputFiles(filePath, { timeout: config.timeout });
- console.debug(`[uploadFileToField] setInputFiles 完成`);
- // 5. 等待上传完成(如果需要)
- if (config.waitForUpload) {
- // 等待一小段时间确保文件上传处理完成
- await page.waitForTimeout(200);
- console.debug(`[uploadFileToField] 上传等待完成`);
- }
- console.debug(`[uploadFileToField] 上传完成`);
- } catch (error) {
- // 选择器无效或其他错误 - 提供详细的上下文信息
- const errorMessage = error instanceof Error ? error.message : '未知错误';
- throwError({
- operation: 'uploadFileToField',
- target: `选择器 "${selector}"`,
- expected: '文件输入框存在于页面且可访问',
- actual: `错误: ${errorMessage}`,
- suggestion: [
- '检查选择器是否正确(推荐使用 data-testid)',
- '确认文件输入框已渲染到页面',
- '确认元素可见且未被隐藏(display: none)',
- '检查是否需要等待页面加载完成'
- ].join('\n ')
- });
- }
- }
- /**
- * 解析 fixtures 文件路径
- *
- * @description
- * 将相对文件名解析为绝对路径。
- * 支持相对于 fixtures 目录的路径和子目录。
- *
- * @internal
- *
- * @param fileName - 文件名(可能包含子目录)
- * @param fixturesDir - fixtures 基础目录
- * @returns 解析后的绝对路径
- */
- function resolveFixturePath(fileName: string, fixturesDir: string): string {
- const normalizedFileName = path.normalize(fileName);
- // 拒绝绝对路径和向上遍历路径
- if (normalizedFileName.startsWith("..") || path.isAbsolute(normalizedFileName)) {
- throwError({
- operation: "uploadFileToField",
- target: fileName,
- suggestion: "文件名必须是相对于 fixtures 目录的路径,不能使用 '..' 或绝对路径"
- });
- }
- // 解析完整路径
- const resolvedPath = path.resolve(path.join(fixturesDir, normalizedFileName));
- const resolvedFixturesDir = path.resolve(fixturesDir);
- // 验证解析后的路径在 fixtures 目录内(防止路径遍历攻击)
- if (!resolvedPath.startsWith(resolvedFixturesDir)) {
- throwError({
- operation: "uploadFileToField",
- target: fileName,
- suggestion: "文件名路径试图访问 fixtures 目录之外的文件"
- });
- }
- return resolvedPath;
- }
|