feat: image upload on articles

This commit is contained in:
fsylvain committed 2026-09-28 02:28:20 +02:00
1 parent d4fcfb2bba
commit 938263cd6a
12 files changed
+1436 -65

No files matched your search

+108
View File
@@ -205,6 +205,63 @@ async function typeExactBodyLength(page: Page, totalLength: number) {
await page.keyboard.insertText(inner)
}
function mib(value: number) {
return Math.round(value * 1024 * 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()
}
async function chooseImageFile(page: Page, filename: string, mimeType: string, size: number) {
await page.locator('input[type="file"]').setInputFiles({
name: filename,
mimeType,
buffer: Buffer.alloc(size),
})
}
async function mockUploadedImage(page: Page) {
const isDark = await page.evaluate(
() => window.matchMedia('(prefers-color-scheme: dark)').matches,
)
const background = isDark ? 'rgb(26,26,26)' : 'rgb(244,244,245)'
const border = isDark ? 'rgb(64,64,64)' : 'rgb(212,212,216)'
await page.route('**/static/uploadedImages/**', async (route) => {
const pathname = new URL(route.request().url()).pathname
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="900" height="450"><rect width="100%" height="100%" fill="${background}" stroke="${border}" stroke-width="2" /><text x="50%" y="50%" text-anchor="middle" dominant-baseline="middle" font-family="monospace" font-size="16" fill="${border}">${pathname}</text></svg>`
await route.fulfill({ status: 200, contentType: 'image/svg+xml', body: svg })
})
}
async function freezeUploadProgress(page: Page, loaded: number, total: number) {
await page.evaluate(
({ loaded, total }) => {
const proto = XMLHttpRequest.prototype as unknown as Record<string, unknown>
const originalOpen = proto.open as (...args: unknown[]) => void
proto.open = function (this: XMLHttpRequest, ...args: unknown[]) {
;(this as unknown as { __imageUrl?: string }).__imageUrl = String(args[1])
return originalOpen.apply(this, args)
}
const originalSend = proto.send as (...args: unknown[]) => void
proto.send = function (this: XMLHttpRequest, ...args: unknown[]) {
const url = (this as unknown as { __imageUrl?: string }).__imageUrl ?? ''
if (url.includes('/image')) {
setTimeout(() => {
const event = new Event('progress')
Object.assign(event, { loaded, total, lengthComputable: true })
this.upload.dispatchEvent(event)
}, 50)
return
}
return originalSend.apply(this, args)
}
},
{ loaded, total },
)
}
const newArticleTitle = 'Choosing tiles for a small bathroom'
const newArticleBody = 'Small rooms show every grout line, so we start with the floor.'
@@ -846,6 +903,57 @@ export const visualStates: Record<string, Record<string, VisualState>> = {
'GET /v1/article/9999': { status: 404 },
},
},
'Upload: choose': {
path: '/article/12/edit',
now: editorNow,
api: editorApi(),
steps: async (page) => {
await markDirty(page)
await openImageUploader(page)
},
},
'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()
},
},
'Upload: rejected': {
path: '/article/12/edit',
now: editorNow,
api: editorApi(),
steps: async (page) => {
await markDirty(page)
await openImageUploader(page)
await chooseImageFile(page, 'north-wall.tif', 'image/png', mib(14.2))
await page.getByText(`"north-wall.tif" can't be used`).waitFor()
},
},
'Upload: inserted': {
path: '/article/12/edit',
now: editorNow,
api: editorApi({
'POST /v1/article/12/image': {
status: 201,
body: { location: '/static/uploadedImages/north-wall.jpg' },
},
}),
steps: async (page) => {
await mockUploadedImage(page)
await page.getByText('add a margin on materials').click()
await page.keyboard.press('Home')
await openImageUploader(page)
await chooseImageFile(page, 'north-wall.jpg', 'image/jpeg', mib(3.9))
await page.getByText('Save to keep it in the article.').first().waitFor()
await page.waitForTimeout(650)
},
},
},
'new-article': {
Default: {