files.spec.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. import { test, expect } from '@playwright/test';
  2. test.describe('Admin File Management', () => {
  3. test.beforeEach(async ({ page }) => {
  4. // Login to admin panel
  5. await page.goto('/admin/login');
  6. await page.fill('input[name="username"]', 'admin');
  7. await page.fill('input[name="password"]', 'password');
  8. await page.click('button[type="submit"]');
  9. await page.waitForURL('/admin/dashboard');
  10. // Navigate to files page
  11. await page.click('a[href="/admin/files"]');
  12. await page.waitForURL('/admin/files');
  13. });
  14. test('should display files list', async ({ page }) => {
  15. // Wait for files to load
  16. await page.waitForSelector('[data-testid="files-table"]');
  17. // Check if table headers are present
  18. await expect(page.locator('th:has-text("文件名")')).toBeVisible();
  19. await expect(page.locator('th:has-text("类型")')).toBeVisible();
  20. await expect(page.locator('th:has-text("大小")')).toBeVisible();
  21. await expect(page.locator('th:has-text("上传时间")')).toBeVisible();
  22. // Check if at least one file is displayed (or empty state)
  23. const filesCount = await page.locator('[data-testid="file-row"]').count();
  24. if (filesCount === 0) {
  25. await expect(page.locator('text=暂无文件')).toBeVisible();
  26. } else {
  27. await expect(page.locator('[data-testid="file-row"]').first()).toBeVisible();
  28. }
  29. });
  30. test('should upload file successfully', async ({ page }) => {
  31. // Click upload button
  32. await page.click('button:has-text("上传文件")');
  33. // Wait for upload modal
  34. await page.waitForSelector('[data-testid="upload-modal"]');
  35. // Create a test file
  36. const randomString = Math.random().toString(36).substring(2, 10);
  37. const testFileName = `test-${randomString}.txt`;
  38. const testFileContent = 'This is a test file content';
  39. // Upload file
  40. const fileInput = page.locator('input[type="file"]');
  41. await fileInput.setInputFiles({
  42. name: testFileName,
  43. mimeType: 'text/plain',
  44. buffer: Buffer.from(testFileContent)
  45. });
  46. // Fill optional fields
  47. await page.fill('input[name="description"]', 'Test file description');
  48. // Submit upload
  49. await page.click('button:has-text("开始上传")');
  50. // Wait for upload to complete
  51. await expect(page.locator('text=上传成功')).toBeVisible({ timeout: 30000 });
  52. // Verify file appears in list
  53. await page.waitForSelector(`[data-testid="file-row"]:has-text("${testFileName}")`);
  54. await expect(page.locator(`text=${testFileName}`)).toBeVisible();
  55. });
  56. test('should search files', async ({ page }) => {
  57. // Assume there are some files already
  58. await page.waitForSelector('[data-testid="files-table"]');
  59. // Use search functionality
  60. const searchTerm = 'document';
  61. await page.fill('input[placeholder="搜索文件"]', searchTerm);
  62. await page.keyboard.press('Enter');
  63. // Wait for search results
  64. await page.waitForLoadState('networkidle');
  65. // Verify search results (either show results or no results message)
  66. const results = await page.locator('[data-testid="file-row"]').count();
  67. if (results === 0) {
  68. await expect(page.locator('text=未找到相关文件')).toBeVisible();
  69. } else {
  70. // Check that all visible files contain search term in name or description
  71. const fileRows = page.locator('[data-testid="file-row"]');
  72. for (let i = 0; i < results; i++) {
  73. const rowText = await fileRows.nth(i).textContent();
  74. expect(rowText?.toLowerCase()).toContain(searchTerm.toLowerCase());
  75. }
  76. }
  77. });
  78. test('should download file', async ({ page }) => {
  79. // Wait for files to load
  80. await page.waitForSelector('[data-testid="file-row"]');
  81. // Get first file row
  82. const firstFile = page.locator('[data-testid="file-row"]').first();
  83. const fileName = await firstFile.locator('[data-testid="file-name"]').textContent();
  84. // Setup download tracking
  85. const downloadPromise = page.waitForEvent('download');
  86. // Click download button
  87. await firstFile.locator('button:has-text("下载")').click();
  88. // Wait for download to start
  89. const download = await downloadPromise;
  90. // Verify download filename
  91. expect(download.suggestedFilename()).toContain(fileName?.trim() || '');
  92. });
  93. test('should delete file', async ({ page }) => {
  94. // Wait for files to load
  95. await page.waitForSelector('[data-testid="file-row"]');
  96. // Get first file row
  97. const firstFile = page.locator('[data-testid="file-row"]').first();
  98. const fileName = await firstFile.locator('[data-testid="file-name"]').textContent();
  99. // Click delete button
  100. await firstFile.locator('button:has-text("删除")').click();
  101. // Confirm deletion in dialog
  102. await page.waitForSelector('[role="dialog"]');
  103. await page.click('button:has-text("确认删除")');
  104. // Wait for deletion to complete
  105. await expect(page.locator('text=删除成功')).toBeVisible();
  106. // Verify file is removed from list
  107. await expect(page.locator(`text=${fileName}`)).not.toBeVisible({ timeout: 5000 });
  108. });
  109. test('should view file details', async ({ page }) => {
  110. // Wait for files to load
  111. await page.waitForSelector('[data-testid="file-row"]');
  112. // Click view details on first file
  113. await page.locator('[data-testid="file-row"]').first().locator('button:has-text("查看")').click();
  114. // Wait for details modal
  115. await page.waitForSelector('[data-testid="file-details-modal"]');
  116. // Verify details are displayed
  117. await expect(page.locator('[data-testid="file-name"]')).toBeVisible();
  118. await expect(page.locator('[data-testid="file-size"]')).toBeVisible();
  119. await expect(page.locator('[data-testid="file-type"]')).toBeVisible();
  120. await expect(page.locator('[data-testid="upload-time"]')).toBeVisible();
  121. // Close modal
  122. await page.click('button[aria-label="Close"]');
  123. });
  124. test('should handle bulk operations', async ({ page }) => {
  125. // Wait for files to load
  126. await page.waitForSelector('[data-testid="file-row"]');
  127. // Select multiple files
  128. const checkboxes = page.locator('input[type="checkbox"][name="file-select"]');
  129. const fileCount = await checkboxes.count();
  130. if (fileCount >= 2) {
  131. // Select first two files
  132. await checkboxes.nth(0).check();
  133. await checkboxes.nth(1).check();
  134. // Verify bulk actions are visible
  135. await expect(page.locator('button:has-text("批量下载")')).toBeVisible();
  136. await expect(page.locator('button:has-text("批量删除")')).toBeVisible();
  137. // Test bulk delete
  138. await page.click('button:has-text("批量删除")');
  139. await page.waitForSelector('[role="dialog"]');
  140. await page.click('button:has-text("确认删除")');
  141. await expect(page.locator('text=删除成功')).toBeVisible();
  142. }
  143. });
  144. test('should handle file upload errors', async ({ page }) => {
  145. // Click upload button
  146. await page.click('button:has-text("上传文件")');
  147. await page.waitForSelector('[data-testid="upload-modal"]');
  148. // Try to upload without selecting file
  149. await page.click('button:has-text("开始上传")');
  150. // Should show validation error
  151. await expect(page.locator('text=请选择要上传的文件')).toBeVisible();
  152. // Close modal
  153. await page.click('button[aria-label="Close"]');
  154. });
  155. test('should paginate files list', async ({ page }) => {
  156. // Wait for files to load
  157. await page.waitForSelector('[data-testid="files-table"]');
  158. // Check if pagination exists
  159. const pagination = page.locator('[data-testid="pagination"]');
  160. if (await pagination.isVisible()) {
  161. // Test next page
  162. await page.click('button:has-text("下一页")');
  163. await page.waitForLoadState('networkidle');
  164. // Test previous page
  165. await page.click('button:has-text("上一页")');
  166. await page.waitForLoadState('networkidle');
  167. // Test specific page
  168. const pageButtons = page.locator('[data-testid="page-button"]');
  169. if (await pageButtons.count() > 0) {
  170. await pageButtons.nth(1).click(); // Click second page
  171. await page.waitForLoadState('networkidle');
  172. }
  173. }
  174. });
  175. test('should filter files by type', async ({ page }) => {
  176. // Wait for files to load
  177. await page.waitForSelector('[data-testid="files-table"]');
  178. // Open filter dropdown
  179. await page.click('button:has-text("筛选")');
  180. await page.waitForSelector('[role="menu"]');
  181. // Filter by image type
  182. await page.click('text=图片');
  183. await page.waitForLoadState('networkidle');
  184. // Verify only images are shown (or no results message)
  185. const fileRows = page.locator('[data-testid="file-row"]');
  186. const rowCount = await fileRows.count();
  187. if (rowCount > 0) {
  188. for (let i = 0; i < rowCount; i++) {
  189. const fileType = await fileRows.nth(i).locator('[data-testid="file-type"]').textContent();
  190. expect(fileType?.toLowerCase()).toMatch(/(image|jpg|jpeg|png|gif|webp)/);
  191. }
  192. } else {
  193. await expect(page.locator('text=未找到图片文件')).toBeVisible();
  194. }
  195. // Clear filter
  196. await page.click('button:has-text("清除筛选")');
  197. await page.waitForLoadState('networkidle');
  198. });
  199. test('should sort files', async ({ page }) => {
  200. // Wait for files to load
  201. await page.waitForSelector('[data-testid="files-table"]');
  202. // Test sorting by name
  203. await page.click('th:has-text("文件名")');
  204. await page.waitForLoadState('networkidle');
  205. // Test sorting by size
  206. await page.click('th:has-text("大小")');
  207. await page.waitForLoadState('networkidle');
  208. // Test sorting by upload time
  209. await page.click('th:has-text("上传时间")');
  210. await page.waitForLoadState('networkidle');
  211. });
  212. });
  213. test.describe('File Management Accessibility', () => {
  214. test('should be keyboard accessible', async ({ page }) => {
  215. await page.goto('/admin/files');
  216. await page.waitForSelector('[data-testid="files-table"]');
  217. // Test tab navigation
  218. await page.keyboard.press('Tab');
  219. await expect(page.locator('button:has-text("上传文件")')).toBeFocused();
  220. // Test keyboard operations on file rows
  221. const firstFileRow = page.locator('[data-testid="file-row"]').first();
  222. await firstFileRow.focus();
  223. await page.keyboard.press('Enter');
  224. await expect(page.locator('[data-testid="file-details-modal"]')).toBeVisible();
  225. // Close modal with Escape
  226. await page.keyboard.press('Escape');
  227. await expect(page.locator('[data-testid="file-details-modal"]')).not.toBeVisible();
  228. });
  229. test('should have proper ARIA labels', async ({ page }) => {
  230. await page.goto('/admin/files');
  231. await page.waitForSelector('[data-testid="files-table"]');
  232. // Check ARIA attributes
  233. await expect(page.locator('[data-testid="files-table"]')).toHaveAttribute('role', 'grid');
  234. await expect(page.locator('th')).toHaveAttribute('role', 'columnheader');
  235. await expect(page.locator('[data-testid="file-row"]')).toHaveAttribute('role', 'row');
  236. // Check button accessibility
  237. const buttons = page.locator('button');
  238. const buttonCount = await buttons.count();
  239. for (let i = 0; i < Math.min(buttonCount, 5); i++) {
  240. const button = buttons.nth(i);
  241. const hasAriaLabel = await button.getAttribute('aria-label');
  242. expect(hasAriaLabel).toBeTruthy();
  243. }
  244. });
  245. });