import { test, expect, type Page, type Request } from '@playwright/test' const BASE_URL = 'http://localhost:5173' const API_URL = 'http://localhost:8080' const TS = Date.now() test.describe.configure({ mode: 'serial' }) let uploadedFileId: number let uploadedFileName: string let subFolderId: number let subFolderName: string test.describe('完整端到端流程', () => { test('1. 创建根级文件夹', async ({ page }) => { await page.goto(`${BASE_URL}/#/files`) await page.waitForLoadState('networkidle') await page.locator('button', { hasText: '新建文件夹' }).click() await expect(page.locator('.el-dialog__header', { hasText: '新建文件夹' })).toBeVisible() subFolderName = `e2e-folder-${TS}` await page.locator('.el-dialog input').fill(subFolderName) await page.locator('.el-dialog button', { hasText: '创建' }).click() await expect(page.locator('.el-message--success')).toBeVisible({ timeout: 5000 }) await expect(page.locator('text=' + subFolderName)).toBeVisible({ timeout: 3000 }) const folderRow = page.locator('span', { hasText: new RegExp(subFolderName) }).first() await expect(folderRow).toBeVisible() }) test('2. 进入文件夹并上传文件', async ({ page }) => { await page.goto(`${BASE_URL}/#/files`) await page.waitForLoadState('networkidle') const folderLink = page.locator('span', { hasText: new RegExp(`e2e-folder-${TS}`) }).first() await folderLink.click() await page.waitForTimeout(500) await expect(page.locator('.el-breadcrumb__item').last()).toContainText(`e2e-folder-${TS}`) const fileInput = page.locator('input[type="file"]') await fileInput.setInputFiles({ name: 'e2e-test-file.txt', mimeType: 'text/plain', buffer: Buffer.from(`Hello E2E Test ${TS}`), }) await page.waitForResponse( resp => resp.url().includes('/api/v1/files/uploads') && resp.status() === 201, { timeout: 10000 } ) await page.waitForResponse( resp => resp.url().includes('/complete') && [200, 201].includes(resp.status()), { timeout: 30000 } ).catch(() => {}) await page.waitForTimeout(1000) const table = page.locator('.el-table') await expect(table).toBeVisible() const rows = table.locator('.el-table__body-wrapper .el-table__row') await expect(rows, 'file should appear in table after upload').toHaveCount(1, { timeout: 10000 }) await expect(rows.first().locator('td').first()).toContainText('e2e-test-file.txt') }) test('3. 验证文件出现在表格中', async ({ page }) => { await page.goto(`${BASE_URL}/#/files`) await page.waitForLoadState('networkidle') const folderLink = page.locator('span', { hasText: new RegExp(`e2e-folder-${TS}`) }).first() await folderLink.click() await page.waitForTimeout(1000) const table = page.locator('.el-table') await expect(table).toBeVisible() const rows = table.locator('.el-table__body-wrapper .el-table__row') await expect(rows).toHaveCount(1, { timeout: 5000 }) const row = rows.first() await expect(row.locator('td').first()).toContainText('e2e-test-file.txt') const sizeCell = row.locator('td').nth(1) await expect(sizeCell).toContainText(/B|KB|MB/) await expect(row.locator('td').nth(2)).toContainText(/text\/plain|application\/octet-stream/) }) test('4. 获取上传文件 ID(通过 API)', async ({ request }) => { const listResp = await request.get(`${API_URL}/api/v1/files?folder_id=2`) const body = await listResp.json() if (body.data?.files?.length > 0) { uploadedFileId = body.data.files[0].id uploadedFileName = body.data.files[0].name } else { const allResp = await request.get(`${API_URL}/api/v1/files?search=e2e-test-file`) const allBody = await allResp.json() expect(allBody.data.files.length).toBeGreaterThanOrEqual(1) uploadedFileId = allBody.data.files[0].id uploadedFileName = allBody.data.files[0].name } expect(uploadedFileId).toBeDefined() expect(uploadedFileId).toBeGreaterThan(0) }) test('5. 下载文件', async ({ page, context }) => { await page.goto(`${BASE_URL}/#/files`) await page.waitForLoadState('networkidle') const folderLink = page.locator('span', { hasText: new RegExp(`e2e-folder-${TS}`) }).first() await folderLink.click() await page.waitForTimeout(1000) const downloadPromise = page.waitForEvent('download', { timeout: 10000 }).catch(() => null) await page.locator('button', { hasText: '下载' }).first().click() const download = await downloadPromise if (download) { const downloadName = download.suggestedFilename() expect(downloadName).toBeTruthy() } }) test('6. 搜索文件', async ({ page }) => { await page.goto(`${BASE_URL}/#/files`) await page.waitForLoadState('networkidle') const searchInput = page.locator('input[placeholder="搜索文件"]') await searchInput.fill('e2e-test-file') const [searchResp] = await Promise.all([ page.waitForResponse( resp => resp.url().includes('/api/v1/files') && resp.url().includes('search='), { timeout: 5000 } ), searchInput.press('Enter'), ]) expect(searchResp.status()).toBe(200) const body = await searchResp.json() expect(body.data.files.length).toBeGreaterThanOrEqual(1) expect(body.data.files[0].name).toContain('e2e-test-file') await expect(page.locator('.el-table__body-wrapper .el-table__row')).toHaveCount( body.data.files.length, { timeout: 3000 } ) await searchInput.fill('') await searchInput.press('Enter') await page.waitForTimeout(500) }) test('7. 面包屑导航 - 返回根目录', async ({ page }) => { await page.goto(`${BASE_URL}/#/files`) await page.waitForLoadState('networkidle') const folderLink = page.locator('span', { hasText: new RegExp(`e2e-folder-${TS}`) }).first() await folderLink.click() await page.waitForTimeout(1000) await expect(page.locator('.el-breadcrumb__item').last()).toContainText(`e2e-folder-${TS}`) await page.locator('.el-breadcrumb__item').first().click() await page.waitForTimeout(1000) await expect(page.locator('.el-breadcrumb__item')).toHaveCount(1) await expect(page.locator('.el-breadcrumb__item').first()).toContainText('全部文件') const rootFolders = page.locator('text=e2e-folder-') await expect(rootFolders.first()).toBeVisible({ timeout: 3000 }) }) test('8. 删除文件', async ({ page }) => { await page.goto(`${BASE_URL}/#/files`) await page.waitForLoadState('networkidle') const folderLink = page.locator('span', { hasText: new RegExp(`e2e-folder-${TS}`) }).first() await folderLink.click() await page.waitForTimeout(1000) const rowCountBefore = await page.locator('.el-table__body-wrapper .el-table__row').count() page.on('dialog', async dialog => { expect(dialog.type()).toBe('confirm') await dialog.accept() }) await page.locator('button', { hasText: '删除' }).first().click() await page.waitForTimeout(500) await page.locator('.el-message-box').locator('button', { hasText: '确定' }).click().catch(() => { }) await expect(page.locator('.el-message--success')).toBeVisible({ timeout: 5000 }) await page.waitForTimeout(1000) const rowCountAfter = await page.locator('.el-table__body-wrapper .el-table__row').count() expect(rowCountAfter).toBe(rowCountBefore - 1) }) test('9. 再上传一个文件(用于 FilePicker 测试)', async ({ request, page }) => { const sha256 = 'a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e' const fileContent = 'Hello World' const initResp = await request.post(`${API_URL}/api/v1/files/uploads`, { data: { file_name: 'picker-test-file.txt', file_size: fileContent.length, sha256, chunk_size: 16777216, }, }) expect([200, 201]).toContain(initResp.status()) const initData = await initResp.json() if (initResp.status() === 201 && initData.data?.total_chunks) { const sessionId = initData.data.id const chunkResp = await request.put(`${API_URL}/api/v1/files/uploads/${sessionId}/chunks/0`, { multipart: { chunk: { name: 'chunk', mimeType: 'text/plain', buffer: Buffer.from(fileContent), }, }, }) expect(chunkResp.status()).toBe(200) const completeResp = await request.post(`${API_URL}/api/v1/files/uploads/${sessionId}/complete`) expect([200, 201]).toContain(completeResp.status()) const completeData = await completeResp.json() uploadedFileId = completeData.data.id uploadedFileName = completeData.data.name } else { uploadedFileId = initData.data.id uploadedFileName = initData.data.name } expect(uploadedFileId).toBeDefined() expect(uploadedFileId).toBeGreaterThan(0) }) test('10. FilePicker 弹窗 - 选择文件', async ({ page }) => { await page.goto(`${BASE_URL}/#/tasks/create`) await page.waitForLoadState('networkidle') await page.waitForResponse( resp => resp.url().includes('/api/v1/applications'), { timeout: 5000 } ).catch(() => null) await page.locator('button', { hasText: '选择文件' }).click() await expect(page.locator('.el-dialog__header', { hasText: '选择文件' })).toBeVisible({ timeout: 3000 }) await expect(page.locator('.el-dialog .el-breadcrumb')).toBeVisible() await expect(page.locator('.el-dialog .el-breadcrumb__item').first()).toContainText('全部文件') await page.waitForTimeout(500) const checkboxes = page.locator('.el-dialog .el-checkbox') const checkboxCount = await checkboxes.count() if (checkboxCount > 0) { await checkboxes.first().click() await expect(page.locator('.el-dialog button', { hasText: /确认/ })).toBeVisible() await page.locator('.el-dialog button', { hasText: /确认/ }).click() await page.waitForTimeout(500) const tags = page.locator('.el-tag') await expect(tags.first()).toBeVisible({ timeout: 3000 }) const firstTag = tags.first() await expect(firstTag.locator('.el-tag__content')).toContainText(/\.txt/) const removeIcon = firstTag.locator('.el-tag__close') await removeIcon.click() await expect(page.locator('.el-tag')).toHaveCount(0, { timeout: 2000 }) } else { await page.locator('.el-dialog button', { hasText: '取消' }).click() } }) test('11. FilePicker 多选测试', async ({ request, page }) => { const files: { id: number; name: string }[] = [] for (let i = 0; i < 3; i++) { const content = `multi-file-${i}-${TS}` const sha256 = await computeSHA256(content) const initResp = await request.post(`${API_URL}/api/v1/files/uploads`, { data: { file_name: content + '.txt', file_size: content.length, sha256, chunk_size: 16777216 }, }) const initData = await initResp.json() if (initResp.status() === 201 && initData.data?.total_chunks) { const sid = initData.data.id await request.put(`${API_URL}/api/v1/files/uploads/${sid}/chunks/0`, { multipart: { chunk: { name: 'chunk', mimeType: 'text/plain', buffer: Buffer.from(content) } }, }) const cResp = await request.post(`${API_URL}/api/v1/files/uploads/${sid}/complete`) const cData = await cResp.json() files.push({ id: cData.data.id, name: cData.data.name }) } else { files.push({ id: initData.data.id, name: initData.data.name }) } } await page.goto(`${BASE_URL}/#/tasks/create`) await page.waitForLoadState('networkidle') await page.waitForResponse(r => r.url().includes('/api/v1/applications'), { timeout: 5000 }).catch(() => null) await page.locator('button', { hasText: '选择文件' }).click() await expect(page.locator('.el-dialog__header', { hasText: '选择文件' })).toBeVisible({ timeout: 3000 }) await page.waitForTimeout(500) const checkboxes = page.locator('.el-dialog .el-checkbox') const count = await checkboxes.count() const toSelect = Math.min(count, 3) for (let i = 0; i < toSelect; i++) { await checkboxes.nth(i).click() } await page.locator('.el-dialog button', { hasText: /确认/ }).click() await page.waitForTimeout(500) if (toSelect > 0) { const tags = page.locator('.el-tag') await expect(tags).toHaveCount(toSelect, { timeout: 3000 }) } }) test('12. 删除文件夹', async ({ page }) => { await page.goto(`${BASE_URL}/#/files`) await page.waitForLoadState('networkidle') const folderLink = page.locator('span', { hasText: new RegExp(`e2e-folder-${TS}`) }).first() const folderRow = folderLink.locator('xpath=ancestor::div[contains(@style, "cursor: pointer")]') const deleteBtn = folderRow.locator('button', { hasText: '删除' }).first() if (await deleteBtn.isVisible().catch(() => false)) { await deleteBtn.click() await page.waitForTimeout(500) const confirmBtn = page.locator('.el-message-box').locator('button', { hasText: '确定' }) if (await confirmBtn.isVisible().catch(() => false)) { await confirmBtn.click() await page.waitForTimeout(2000) const successMsg = page.locator('.el-message--success') if (await successMsg.isVisible().catch(() => false)) { } else { const errorMsg = page.locator('.el-message--error') if (await errorMsg.isVisible().catch(() => false)) { } } } } }) }) async function computeSHA256(content: string): Promise { const crypto = await import('crypto') return crypto.createHash('sha256').update(content).digest('hex') }