test(web): add Playwright config and E2E tests for file manager
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
374
web/tests/e2e-flow.spec.ts
Normal file
374
web/tests/e2e-flow.spec.ts
Normal file
@@ -0,0 +1,374 @@
|
||||
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')
|
||||
}
|
||||
Reference in New Issue
Block a user