feat: image cropping / resizing T0

This commit is contained in:
fsylvain committed 2026-09-28 07:46:12 +02:00
1 parent cb05b38f36
commit e15f444603
18 files changed
+1869 -46

No files matched your search

+163 -4
View File
@@ -209,6 +209,10 @@ function mib(value: number) {
return Math.round(value * 1024 * 1024)
}
function kb(value: number) {
return Math.round(value * 1024)
}
async function openImageUploader(page: Page) {
await page.getByRole('button', { name: 'Image', exact: true }).first().click()
await page.getByRole('dialog').getByText('Add an image').waitFor()
@@ -222,6 +226,130 @@ async function chooseImageFile(page: Page, filename: string, mimeType: string, s
})
}
async function chooseRasterImageFile(
page: Page,
filename: string,
mimeType: string,
naturalWidth: number,
naturalHeight: number,
sizeBytes: number,
) {
await page.evaluate(
async ({ filename, mimeType, naturalWidth, naturalHeight, sizeBytes }) => {
const canvas = document.createElement('canvas')
canvas.width = naturalWidth
canvas.height = naturalHeight
const ctx = canvas.getContext('2d')!
ctx.fillStyle = '#d8d0c3'
ctx.fillRect(0, 0, naturalWidth, naturalHeight)
ctx.fillStyle = '#8f8373'
ctx.fillRect(0, naturalHeight * 0.68, naturalWidth, naturalHeight * 0.32)
ctx.fillStyle = '#f1ece3'
ctx.strokeStyle = '#c4b9a8'
ctx.lineWidth = naturalWidth * 0.015
ctx.fillRect(
naturalWidth * 0.56,
naturalHeight * 0.14,
naturalWidth * 0.22,
naturalHeight * 0.42,
)
ctx.strokeRect(
naturalWidth * 0.56,
naturalHeight * 0.14,
naturalWidth * 0.22,
naturalHeight * 0.42,
)
const blob = await new Promise<Blob>((resolve) =>
canvas.toBlob((b) => resolve(b!), mimeType, 0.85),
)
const file = new File([blob], filename, { type: mimeType })
Object.defineProperty(file, 'size', { value: sizeBytes, configurable: true })
const dataTransfer = new DataTransfer()
dataTransfer.items.add(file)
const input = document.querySelector('input[type="file"]') as HTMLInputElement
input.files = dataTransfer.files
input.dispatchEvent(new Event('change', { bubbles: true }))
input.dispatchEvent(new Event('input', { bubbles: true }))
},
{ filename, mimeType, naturalWidth, naturalHeight, sizeBytes },
)
}
async function openCropStep(
page: Page,
filename: string,
mimeType: string,
naturalWidth: number,
naturalHeight: number,
sizeBytes: number,
) {
await openImageUploader(page)
await chooseRasterImageFile(page, filename, mimeType, naturalWidth, naturalHeight, sizeBytes)
await page.getByRole('dialog').getByText('Crop and resize').waitFor()
}
async function selectAspectRatio(page: Page, ratio: string) {
await page
.getByRole('radiogroup', { name: 'Aspect ratio' })
.getByRole('radio', { name: ratio, exact: true })
.click()
}
async function resizeCropFrameTo(
page: Page,
naturalWidth: number,
targetWidthPx: number,
targetHeightPx: number,
) {
const frame = page.locator('[data-testid="crop-frame"]')
const handle = page.locator('[data-testid="crop-handle"]').nth(3)
async function currentSize() {
const box = (await frame.boundingBox())!
const stageWidth = await frame.evaluate(
(el) => el.parentElement!.getBoundingClientRect().width,
)
const scale = stageWidth / naturalWidth
return {
width: Math.round(box.width / scale),
height: Math.round(box.height / scale),
scale,
}
}
for (let attempt = 0; attempt < 6; attempt += 1) {
const frameBox = (await frame.boundingBox())!
const handleBox = (await handle.boundingBox())!
const { width: currentWidth, height: currentHeight, scale } = await currentSize()
if (
Math.abs(currentWidth - targetWidthPx) <= 30 &&
Math.abs(currentHeight - targetHeightPx) <= 30
) {
break
}
const startX = handleBox.x + handleBox.width / 2
const startY = handleBox.y + handleBox.height / 2
const targetX = frameBox.x + targetWidthPx * scale
const targetY = frameBox.y + targetHeightPx * scale
await page.mouse.move(startX, startY)
await page.mouse.down()
await page.mouse.move(targetX, targetY, { steps: 1 })
await page.mouse.up()
await page.waitForTimeout(50)
}
for (let guard = 0; guard < 60; guard += 1) {
const { width: currentWidth, height: currentHeight } = await currentSize()
if (currentWidth === targetWidthPx && currentHeight === targetHeightPx) {
return
}
if (currentWidth !== targetWidthPx) {
await frame.press(currentWidth > targetWidthPx ? 'Shift+ArrowLeft' : 'Shift+ArrowRight')
await page.waitForTimeout(120)
continue
}
await frame.press(currentHeight > targetHeightPx ? 'Shift+ArrowUp' : 'Shift+ArrowDown')
await page.waitForTimeout(120)
}
}
async function mockUploadedImage(page: Page) {
const isDark = await page.evaluate(
() => window.matchMedia('(prefers-color-scheme: dark)').matches,
@@ -957,16 +1085,47 @@ export const visualStates: Record<string, Record<string, VisualState>> = {
await openImageUploader(page)
},
},
'Upload: crop and resize': {
path: '/article/12/edit',
now: editorNow,
api: editorApi(),
steps: async (page) => {
await markDirty(page)
await openCropStep(page, 'north-wall.jpg', 'image/jpeg', 4032, 3024, mib(3.9))
},
},
'Upload: crop to a ratio': {
path: '/article/12/edit',
now: editorNow,
api: editorApi(),
steps: async (page) => {
await markDirty(page)
await openCropStep(page, 'north-wall.jpg', 'image/jpeg', 4032, 3024, mib(3.9))
await selectAspectRatio(page, '16:9')
},
},
'Upload: crop narrower than the width': {
path: '/article/12/edit',
now: editorNow,
api: editorApi(),
steps: async (page) => {
await markDirty(page)
await openCropStep(page, 'north-wall.jpg', 'image/jpeg', 4032, 3024, mib(3.9))
await selectAspectRatio(page, '1:1')
await resizeCropFrameTo(page, 4032, 640, 640)
await page.getByText('The crop is 640 px wide').waitFor()
},
},
'Upload: in progress': {
path: '/article/12/edit',
now: editorNow,
api: editorApi(),
steps: async (page) => {
await markDirty(page)
await openImageUploader(page)
await freezeUploadProgress(page, mib(2.4), mib(3.9))
await chooseImageFile(page, 'north-wall.jpg', 'image/jpeg', mib(3.9))
await page.getByText('north-wall.jpg 62%').waitFor()
await openCropStep(page, 'north-wall.jpg', 'image/jpeg', 4032, 3024, mib(3.9))
await freezeUploadProgress(page, kb(180), kb(420))
await page.getByRole('dialog').getByRole('button', { name: 'Upload image' }).click()
await page.getByText('north-wall.jpg 43%').waitFor()
},
},
'Upload: rejected': {