Files
hpc/web/tests/e2e-flow.spec.ts
2026-04-20 10:40:05 +08:00

375 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string> {
const crypto = await import('crypto')
return crypto.createHash('sha256').update(content).digest('hex')
}