feat(web): whole-page image drop, projected intake limits, and thumbnail tiling
Second alignment step for #2248: document-level drag intake behind the new DropOverlay atom, lightbox close icon and shared dialog mask, DeepSeek Chat thumbnail rules (single 240px long edge with ratio clamp, 64px tiles, merged consecutive assistant images), image limits raised to 20/10MiB/100MiB and published to clients as the imageLimits projection, whole-batch intake pre-check with product-copy banners, and attachment-error reasons mapped to localized copy.
This commit is contained in:
@@ -143,8 +143,55 @@ it('accepts pasted images into the composer rail in order and removes them', asy
|
||||
},
|
||||
})
|
||||
const toast = await screen.findByRole('alert')
|
||||
expect(toast.textContent).toContain('Unsupported image format: text/plain')
|
||||
expect(toast.textContent).toContain('Only PNG, JPG, WebP, and GIF images are supported')
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('alert')).toBeNull()
|
||||
}, { timeout: 6_000 })
|
||||
})
|
||||
|
||||
it('accepts a whole-page drop under the limits-labeled overlay and refuses an over-limit batch at intake', async () => {
|
||||
mountAssembledApp()
|
||||
|
||||
const tree = await screen.findByRole('tree', { name: 'Sessions' }, { timeout: 10_000 })
|
||||
const start = tree.querySelector<HTMLButtonElement>('button[aria-label="New session in fixture"]')
|
||||
if (start === null) throw new Error('fixture Workspace new-session action missing')
|
||||
fireEvent.click(start)
|
||||
const textarea = await screen.findByPlaceholderText('Describe what you want to build', {}, { timeout: 10_000 })
|
||||
|
||||
// A file drag anywhere over the page raises the full-viewport overlay whose
|
||||
// desc line carries the projected limits — copy that can only render after
|
||||
// the imageLimits projection crossed the real fixture transport.
|
||||
const image = new File([new Uint8Array([137, 80, 78, 71])], 'dropped.png', { type: 'image/png' })
|
||||
const dataTransfer = { types: ['Files'], files: [image], dropEffect: 'none' }
|
||||
fireEvent.dragEnter(document.body, { dataTransfer })
|
||||
const overlay = await screen.findByRole('status')
|
||||
expect(overlay.textContent).toContain('Drag images here to add them')
|
||||
await waitFor(() => {
|
||||
expect(overlay.textContent).toContain('Up to 20 images, 10MB each')
|
||||
})
|
||||
|
||||
// Dropping on the transcript area (not the composer card) lands in the rail.
|
||||
fireEvent.drop(document.body, { dataTransfer })
|
||||
await waitFor(() => {
|
||||
const rail = document.querySelector('[role="group"][aria-label="Pending images"]')
|
||||
if (rail === null) throw new Error('attachment rail missing after page drop')
|
||||
expect([...rail.querySelectorAll('img')].map(img => img.getAttribute('alt'))).toEqual(['dropped.png'])
|
||||
}, { timeout: 5_000 })
|
||||
expect(screen.queryByRole('status')).toBeNull()
|
||||
|
||||
// An intake that would exceed the projected per-message count is refused as
|
||||
// a whole batch at add time: the banner names the limit and the rail keeps
|
||||
// only the previously accepted thumbnail — no submit-time rollback.
|
||||
const batch = Array.from({ length: 20 }, (_, i) =>
|
||||
new File([new Uint8Array([137, 80, 78, 71])], `bulk-${String(i)}.png`, { type: 'image/png' }))
|
||||
fireEvent.paste(textarea, {
|
||||
clipboardData: {
|
||||
items: batch.map(file => ({ kind: 'file', type: 'image/png', getAsFile: () => file })),
|
||||
getData: () => '',
|
||||
},
|
||||
})
|
||||
const banner = await screen.findByRole('alert')
|
||||
expect(banner.textContent).toContain('A message can include up to 20 images')
|
||||
const rail = document.querySelector('[role="group"][aria-label="Pending images"]')
|
||||
expect([...(rail?.querySelectorAll('img') ?? [])]).toHaveLength(1)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user