mirror of
https://github.com/saltbo/zpan.git
synced 2026-09-19 01:51:11 +08:00
Resolve #448 — one upload entry point and an AIP-164 soft delete. Upload: POST /objects now returns size-decided upload instructions { sessionId, partSize, urls }; the server picks single PutObject (<=5 GiB) vs 5 GiB-part multipart (>5 GiB) and rejects >5 TiB. The client PUTs each slice, reads its ETag, then POSTs them to POST /objects/{id}/uploads/{sid}/completions (returns the live object). DELETE /objects/{id}/uploads/{sid} aborts and discards the draft. Trash: matters.status drops 'trashed' (enum is {draft,active}); trash is tracked by the existing trashedAt timestamp. DELETE /objects/{id} now soft-deletes; the recycle bin lives under /trash/objects (list roots, get, restorations, purge). Empty-trash is a frontend loop over roots. BREAKING CHANGE: - removes PUT /objects/{id}/status and POST /objects/{id}/uploads - PUT .../uploads/{sid}/status -> POST .../uploads/{sid}/completions {parts} - DELETE /objects/{id} flips hard-purge -> soft-delete; permanent purge moves to DELETE /trash/objects/{id} - DELETE /trash removed; restore is POST /trash/objects/{id}/restorations - matters.status enum loses 'trashed' (migration backfills to trashedAt) The migration swaps the matters_active_name_uniq partial index to exclude trashed rows (WHERE status='active' AND trashed_at IS NULL). The single-PUT presign is header-free so the uniform slice uploader's raw PUT matches the S3 signature. Go downloader client + agent reworked to the unified flow. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
74 lines
2.5 KiB
TypeScript
74 lines
2.5 KiB
TypeScript
import { expect, test } from '@playwright/test'
|
|
import { signUpAndGoToFiles } from './helpers'
|
|
|
|
function makeFiles(count: number) {
|
|
return Array.from({ length: count }, (_, index) => ({
|
|
name: `very-long-mobile-upload-file-name-${index}-${Date.now()}-that-should-truncate-in-the-uploader-panel.txt`,
|
|
mimeType: 'text/plain',
|
|
buffer: Buffer.from(`upload fixture ${index}`),
|
|
}))
|
|
}
|
|
|
|
async function expectNoHorizontalOverflow(page: import('@playwright/test').Page) {
|
|
const hasHScroll = await page.evaluate(
|
|
() => document.documentElement.scrollWidth > document.documentElement.clientWidth,
|
|
)
|
|
expect(hasHScroll).toBe(false)
|
|
}
|
|
|
|
test.describe('Uploader responsive behavior', () => {
|
|
test('mobile: multi-file upload opens uploader, truncates long names, and scrolls vertically @mobile', async ({
|
|
page,
|
|
}) => {
|
|
await signUpAndGoToFiles(page)
|
|
|
|
await page.route('**/*', async (route) => {
|
|
if (route.request().method() === 'PUT') {
|
|
// The unified slice uploader reads the ETag from each S3 PUT response.
|
|
await route.fulfill({ status: 200, headers: { ETag: '"e2e-etag"' }, body: '' })
|
|
return
|
|
}
|
|
await route.continue()
|
|
})
|
|
|
|
const files = makeFiles(12)
|
|
await page.locator('input[type="file"]').first().setInputFiles(files)
|
|
|
|
const popover = page.getByTestId('upload-popover')
|
|
await expect(popover).toBeVisible({ timeout: 10000 })
|
|
await expect(popover).toContainText('Uploads')
|
|
await expect(popover).toContainText(files[0].name)
|
|
|
|
const taskList = page.getByTestId('upload-task-list')
|
|
await expect(taskList).toBeVisible()
|
|
|
|
await expectNoHorizontalOverflow(page)
|
|
|
|
const layout = await taskList.evaluate((el) => {
|
|
const styles = window.getComputedStyle(el)
|
|
return {
|
|
overflowX: styles.overflowX,
|
|
overflowY: styles.overflowY,
|
|
scrollsVertically: el.scrollHeight > el.clientHeight,
|
|
}
|
|
})
|
|
expect(layout.overflowX).toBe('hidden')
|
|
expect(layout.overflowY).toBe('auto')
|
|
expect(layout.scrollsVertically).toBe(true)
|
|
|
|
const firstFileName = page.getByText(files[0].name)
|
|
await expect(firstFileName).toBeVisible()
|
|
const fileNameLayout = await firstFileName.evaluate((el) => {
|
|
const styles = window.getComputedStyle(el)
|
|
return {
|
|
overflow: styles.overflow,
|
|
textOverflow: styles.textOverflow,
|
|
whiteSpace: styles.whiteSpace,
|
|
}
|
|
})
|
|
expect(fileNameLayout.overflow).toBe('hidden')
|
|
expect(fileNameLayout.textOverflow).toBe('ellipsis')
|
|
expect(fileNameLayout.whiteSpace).toBe('nowrap')
|
|
})
|
|
})
|