|
@@ -528,6 +528,257 @@ describe('uploadFileToField - 文件上传工具', () => {
|
|
|
});
|
|
});
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
+ describe('Task 5: 多文件上传测试', () => {
|
|
|
|
|
+ describe('Subtask 5.1: 单文件上传(向后兼容)', () => {
|
|
|
|
|
+ it('应该保持向后兼容(单文件上传仍然工作)', async () => {
|
|
|
|
|
+ // Arrange
|
|
|
|
|
+ const fileName = 'sample-id-card.jpg';
|
|
|
|
|
+ const selector = 'photo-upload';
|
|
|
|
|
+
|
|
|
|
|
+ // Act
|
|
|
|
|
+ await uploadFileToField(mockPage, selector, fileName);
|
|
|
|
|
+
|
|
|
|
|
+ // Assert
|
|
|
|
|
+ expect(mockPage.locator).toHaveBeenCalledWith(selector);
|
|
|
|
|
+ expect(mockLocator.setInputFiles).toHaveBeenCalledWith(
|
|
|
|
|
+ expect.stringContaining(fileName),
|
|
|
|
|
+ { timeout: DEFAULT_TIMEOUTS.static }
|
|
|
|
|
+ );
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it('单文件上传应该传递字符串参数给 setInputFiles', async () => {
|
|
|
|
|
+ // Arrange
|
|
|
|
|
+ const fileName = 'test.jpg';
|
|
|
|
|
+
|
|
|
|
|
+ // Act
|
|
|
|
|
+ await uploadFileToField(mockPage, 'input', fileName);
|
|
|
|
|
+
|
|
|
|
|
+ // Assert
|
|
|
|
|
+ const filePathArg = mockLocator.setInputFiles.mock.calls[0][0];
|
|
|
|
|
+ expect(typeof filePathArg).toBe('string');
|
|
|
|
|
+ expect(filePathArg).toContain(fileName);
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ describe('Subtask 5.2: 多文件上传(2-3 个文件)', () => {
|
|
|
|
|
+ it('应该成功上传多个文件(3个文件)', async () => {
|
|
|
|
|
+ // Arrange
|
|
|
|
|
+ const fileNames = [
|
|
|
|
|
+ 'images/sample-id-card.jpg',
|
|
|
|
|
+ 'images/sample-disability-card.jpg',
|
|
|
|
|
+ 'images/sample-photo.jpg'
|
|
|
|
|
+ ];
|
|
|
|
|
+ const selector = 'photo-upload';
|
|
|
|
|
+
|
|
|
|
|
+ // Act
|
|
|
|
|
+ await uploadFileToField(mockPage, selector, fileNames);
|
|
|
|
|
+
|
|
|
|
|
+ // Assert
|
|
|
|
|
+ expect(mockPage.locator).toHaveBeenCalledWith(selector);
|
|
|
|
|
+ expect(mockLocator.setInputFiles).toHaveBeenCalledWith(
|
|
|
|
|
+ expect.any(Array),
|
|
|
|
|
+ { timeout: DEFAULT_TIMEOUTS.static }
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ // 验证传入的是文件路径数组
|
|
|
|
|
+ const filePathsArg = mockLocator.setInputFiles.mock.calls[0][0];
|
|
|
|
|
+ expect(Array.isArray(filePathsArg)).toBe(true);
|
|
|
|
|
+ expect(filePathsArg).toHaveLength(3);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it('应该为每个文件解析正确的路径', async () => {
|
|
|
|
|
+ // Arrange
|
|
|
|
|
+ const fileNames = ['test1.jpg', 'test2.jpg'];
|
|
|
|
|
+
|
|
|
|
|
+ // Act
|
|
|
|
|
+ await uploadFileToField(mockPage, 'input', fileNames);
|
|
|
|
|
+
|
|
|
|
|
+ // Assert
|
|
|
|
|
+ const filePathsArg = mockLocator.setInputFiles.mock.calls[0][0];
|
|
|
|
|
+ expect(filePathsArg[0]).toContain('test1.jpg');
|
|
|
|
|
+ expect(filePathsArg[1]).toContain('test2.jpg');
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it('应该支持两个文件的上传', async () => {
|
|
|
|
|
+ // Arrange
|
|
|
|
|
+ const fileNames = ['front.jpg', 'back.jpg'];
|
|
|
|
|
+
|
|
|
|
|
+ // Act
|
|
|
|
|
+ await uploadFileToField(mockPage, 'input', fileNames);
|
|
|
|
|
+
|
|
|
|
|
+ // Assert
|
|
|
|
|
+ const filePathsArg = mockLocator.setInputFiles.mock.calls[0][0];
|
|
|
|
|
+ expect(filePathsArg).toHaveLength(2);
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ describe('Subtask 5.3: 文件不存在错误(包含路径列表)', () => {
|
|
|
|
|
+ it('应该在多文件中有文件不存在时抛出错误', async () => {
|
|
|
|
|
+ // Arrange
|
|
|
|
|
+ mockExistsSync.mockImplementation((path: string) => {
|
|
|
|
|
+ // 只有第一个文件存在
|
|
|
|
|
+ return path.includes('exist.jpg');
|
|
|
|
|
+ });
|
|
|
|
|
+ const fileNames = ['exist.jpg', 'missing.jpg', 'also-missing.jpg'];
|
|
|
|
|
+
|
|
|
|
|
+ // Act & Assert
|
|
|
|
|
+ await expect(
|
|
|
|
|
+ uploadFileToField(mockPage, 'input', fileNames)
|
|
|
|
|
+ ).rejects.toThrow(E2ETestError);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it('多文件错误消息应该列出所有缺失文件', async () => {
|
|
|
|
|
+ // Arrange
|
|
|
|
|
+ mockExistsSync.mockImplementation((path: string) => {
|
|
|
|
|
+ // 只有 exist.jpg 存在
|
|
|
|
|
+ return path.includes('exist.jpg');
|
|
|
|
|
+ });
|
|
|
|
|
+ const fileNames = ['exist.jpg', 'missing1.jpg', 'missing2.jpg'];
|
|
|
|
|
+
|
|
|
|
|
+ // Act & Assert
|
|
|
|
|
+ try {
|
|
|
|
|
+ await uploadFileToField(mockPage, 'input', fileNames);
|
|
|
|
|
+ expect.fail('应该抛出错误');
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ expect(error).toBeInstanceOf(E2ETestError);
|
|
|
|
|
+ const e2eError = error as E2ETestError;
|
|
|
|
|
+ expect(e2eError.context.actual).toContain('missing1.jpg');
|
|
|
|
|
+ expect(e2eError.context.actual).toContain('missing2.jpg');
|
|
|
|
|
+ // 错误消息应该包含可用文件
|
|
|
|
|
+ expect(e2eError.context.suggestion).toContain('exist.jpg');
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it('应该在所有文件都不存在时列出所有缺失文件', async () => {
|
|
|
|
|
+ // Arrange
|
|
|
|
|
+ mockExistsSync.mockReturnValue(false);
|
|
|
|
|
+ const fileNames = ['missing1.jpg', 'missing2.jpg', 'missing3.jpg'];
|
|
|
|
|
+
|
|
|
|
|
+ // Act & Assert
|
|
|
|
|
+ try {
|
|
|
|
|
+ await uploadFileToField(mockPage, 'input', fileNames);
|
|
|
|
|
+ expect.fail('应该抛出错误');
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ expect(error).toBeInstanceOf(E2ETestError);
|
|
|
|
|
+ const e2eError = error as E2ETestError;
|
|
|
|
|
+ expect(e2eError.context.actual).toContain('missing1.jpg');
|
|
|
|
|
+ expect(e2eError.context.actual).toContain('missing2.jpg');
|
|
|
|
|
+ expect(e2eError.context.actual).toContain('missing3.jpg');
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ describe('Subtask 5.4: 空数组错误处理', () => {
|
|
|
|
|
+ it('应该在空数组时抛出错误', async () => {
|
|
|
|
|
+ // Arrange
|
|
|
|
|
+ const emptyFileNames: string[] = [];
|
|
|
|
|
+
|
|
|
|
|
+ // Act & Assert
|
|
|
|
|
+ await expect(
|
|
|
|
|
+ uploadFileToField(mockPage, 'input', emptyFileNames)
|
|
|
|
|
+ ).rejects.toThrow(E2ETestError);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it('空数组错误消息应该包含清晰的提示', async () => {
|
|
|
|
|
+ // Arrange
|
|
|
|
|
+ const emptyFileNames: string[] = [];
|
|
|
|
|
+
|
|
|
|
|
+ // Act & Assert
|
|
|
|
|
+ try {
|
|
|
|
|
+ await uploadFileToField(mockPage, 'input', emptyFileNames);
|
|
|
|
|
+ expect.fail('应该抛出错误');
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ expect(error).toBeInstanceOf(E2ETestError);
|
|
|
|
|
+ const e2eError = error as E2ETestError;
|
|
|
|
|
+ expect(e2eError.context.operation).toBe('uploadFileToField');
|
|
|
|
|
+ expect(e2eError.context.suggestion).toContain('不能为空');
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ describe('多文件上传配置选项', () => {
|
|
|
|
|
+ it('多文件上传应该支持自定义 fixtures 目录', async () => {
|
|
|
|
|
+ // Arrange
|
|
|
|
|
+ const customFixturesDir = 'custom/fixtures';
|
|
|
|
|
+ const fileNames = ['test1.jpg', 'test2.jpg'];
|
|
|
|
|
+
|
|
|
|
|
+ // Act
|
|
|
|
|
+ await uploadFileToField(mockPage, 'input', fileNames, {
|
|
|
|
|
+ fixturesDir: customFixturesDir
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // Assert
|
|
|
|
|
+ const filePathsArg = mockLocator.setInputFiles.mock.calls[0][0];
|
|
|
|
|
+ expect(filePathsArg[0]).toContain(customFixturesDir);
|
|
|
|
|
+ expect(filePathsArg[1]).toContain(customFixturesDir);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it('多文件上传应该支持自定义超时', async () => {
|
|
|
|
|
+ // Arrange
|
|
|
|
|
+ const customTimeout = 8000;
|
|
|
|
|
+ const fileNames = ['test1.jpg', 'test2.jpg'];
|
|
|
|
|
+
|
|
|
|
|
+ // Act
|
|
|
|
|
+ await uploadFileToField(mockPage, 'input', fileNames, {
|
|
|
|
|
+ timeout: customTimeout
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // Assert
|
|
|
|
|
+ expect(mockLocator.setInputFiles).toHaveBeenCalledWith(
|
|
|
|
|
+ expect.any(Array),
|
|
|
|
|
+ { timeout: customTimeout }
|
|
|
|
|
+ );
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it('多文件上传应该支持 waitForUpload: false', async () => {
|
|
|
|
|
+ // Arrange
|
|
|
|
|
+ const fileNames = ['test1.jpg', 'test2.jpg'];
|
|
|
|
|
+
|
|
|
|
|
+ // Act
|
|
|
|
|
+ await uploadFileToField(mockPage, 'input', fileNames, {
|
|
|
|
|
+ waitForUpload: false
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // Assert
|
|
|
|
|
+ expect(mockPage.waitForTimeout).not.toHaveBeenCalled();
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ describe('多文件上传错误处理增强', () => {
|
|
|
|
|
+ it('选择器错误应该包含多文件支持提示', async () => {
|
|
|
|
|
+ // Arrange
|
|
|
|
|
+ mockLocator.setInputFiles.mockRejectedValue(
|
|
|
|
|
+ new Error('Element not found')
|
|
|
|
|
+ );
|
|
|
|
|
+ const fileNames = ['test1.jpg', 'test2.jpg'];
|
|
|
|
|
+
|
|
|
|
|
+ // Act & Assert
|
|
|
|
|
+ try {
|
|
|
|
|
+ await uploadFileToField(mockPage, 'bad-selector', fileNames);
|
|
|
|
|
+ expect.fail('应该抛出错误');
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ expect(error).toBeInstanceOf(E2ETestError);
|
|
|
|
|
+ const e2eError = error as E2ETestError;
|
|
|
|
|
+ expect(e2eError.context.suggestion).toContain('multiple');
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it('多文件路径遍历攻击应该被拒绝(第一个文件)', async () => {
|
|
|
|
|
+ // Arrange & Act & Assert
|
|
|
|
|
+ await expect(
|
|
|
|
|
+ uploadFileToField(mockPage, 'input', ['../../../etc/passwd', 'test.jpg'])
|
|
|
|
|
+ ).rejects.toThrow(E2ETestError);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it('多文件路径遍历攻击应该被拒绝(第二个文件)', async () => {
|
|
|
|
|
+ // Arrange & Act & Assert
|
|
|
|
|
+ await expect(
|
|
|
|
|
+ uploadFileToField(mockPage, 'input', ['test.jpg', '../../../etc/passwd'])
|
|
|
|
|
+ ).rejects.toThrow(E2ETestError);
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
describe('主导出验证 (index.ts)', () => {
|
|
describe('主导出验证 (index.ts)', () => {
|
|
|
it('应该正确导出 uploadFileToField 函数', () => {
|
|
it('应该正确导出 uploadFileToField 函数', () => {
|
|
|
expect(uploadFileToField).toBeDefined();
|
|
expect(uploadFileToField).toBeDefined();
|