|
|
@@ -0,0 +1,310 @@
|
|
|
+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();
|
|
|
+ }
|
|
|
+ });
|
|
|
+});
|