Files
zpan/e2e/upload.spec.ts
T
Jasper VanandClaude Opus 4.8 7b8c8c915e refactor(api)!: unify object upload + rework delete/trash lifecycle (#448) (#454)
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>
2026-06-18 21:21:43 -04:00

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')
})
})