feat: image cropping / resizing T0
This commit is contained in:
1 parent
cb05b38f36
commit
e15f444603
18 files changed
+1869
-46
No files matched your search
@@ -13,6 +13,15 @@ vi.mock('@/lib/imageUpload', async (importOriginal) => {
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/lib/imageCrop', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/lib/imageCrop')>()
|
||||
return {
|
||||
...actual,
|
||||
readNaturalImageSize: vi.fn(async () => ({ width: 4032, height: 3024 })),
|
||||
renderCroppedImage: vi.fn(async () => new Blob([new Uint8Array(430080)])),
|
||||
}
|
||||
})
|
||||
|
||||
class FakeXMLHttpRequest {
|
||||
static instances: FakeXMLHttpRequest[] = []
|
||||
|
||||
@@ -141,6 +150,7 @@ async function chooseFile(file: File) {
|
||||
input.dispatchEvent(new Event('change'))
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
await flushPromises()
|
||||
}
|
||||
|
||||
function findButton(text: string) {
|
||||
@@ -153,6 +163,12 @@ async function clickButton(text: string) {
|
||||
findButton(text)?.click()
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
await flushPromises()
|
||||
}
|
||||
|
||||
async function chooseRasterFileAndConfirmUpload(file: File) {
|
||||
await chooseFile(file)
|
||||
await clickButton('Upload image')
|
||||
}
|
||||
|
||||
const RealXMLHttpRequest = global.XMLHttpRequest
|
||||
@@ -247,25 +263,25 @@ describe('"Choose another file" from the rejected state returns to idle', () =>
|
||||
})
|
||||
|
||||
describe('A valid upload shows progress and its cancel action reads "Cancel upload"', () => {
|
||||
it('sends the file and reports live progress', async () => {
|
||||
it('sends the resized file and reports live progress', async () => {
|
||||
const wrapper = mountUploader()
|
||||
await openModal(wrapper)
|
||||
|
||||
await chooseFile(fileFixture('north-wall.jpg', 'image/jpeg', 3.9))
|
||||
await chooseRasterFileAndConfirmUpload(fileFixture('north-wall.jpg', 'image/jpeg', 3.9))
|
||||
|
||||
expect(FakeXMLHttpRequest.instances).toHaveLength(1)
|
||||
const xhr = latestXHR()
|
||||
expect(xhr.url).toBe('/v1/article/12/image')
|
||||
expect(xhr.sentBody?.get('file')).toBeInstanceOf(File)
|
||||
expect(document.body.textContent).toContain('north-wall.jpg 0%')
|
||||
expect(document.body.textContent).toContain('Uploading 0.0 of 3.9 MB')
|
||||
expect(document.body.textContent).toContain('Uploading 0.0 of 420 KB')
|
||||
expect(findButton('Cancel upload')).toBeTruthy()
|
||||
|
||||
xhr.fireProgress(mib(2.4), mib(3.9))
|
||||
xhr.fireProgress(180 * 1024, 420 * 1024)
|
||||
await nextTick()
|
||||
|
||||
expect(document.body.textContent).toContain('north-wall.jpg 62%')
|
||||
expect(document.body.textContent).toContain('Uploading 2.4 of 3.9 MB')
|
||||
expect(document.body.textContent).toContain('north-wall.jpg 43%')
|
||||
expect(document.body.textContent).toContain('Uploading 180 of 420 KB')
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
@@ -274,7 +290,7 @@ describe('Closing the dialog during an upload cancels it', () => {
|
||||
it('aborts the in-flight XMLHttpRequest and closes the modal', async () => {
|
||||
const wrapper = mountUploader()
|
||||
await openModal(wrapper)
|
||||
await chooseFile(fileFixture('north-wall.jpg', 'image/jpeg', 3.9))
|
||||
await chooseRasterFileAndConfirmUpload(fileFixture('north-wall.jpg', 'image/jpeg', 3.9))
|
||||
const xhr = latestXHR()
|
||||
|
||||
await clickButton('Cancel upload')
|
||||
@@ -290,7 +306,7 @@ describe('A successful upload emits the location and auto-closes 600 ms later',
|
||||
vi.useFakeTimers()
|
||||
const wrapper = mountUploader()
|
||||
await openModal(wrapper)
|
||||
await chooseFile(fileFixture('north-wall.jpg', 'image/jpeg', 3.9))
|
||||
await chooseRasterFileAndConfirmUpload(fileFixture('north-wall.jpg', 'image/jpeg', 3.9))
|
||||
const xhr = latestXHR()
|
||||
|
||||
xhr.resolve(201, JSON.stringify({ location: '/static/uploadedImages/north-wall.jpg' }))
|
||||
@@ -319,7 +335,7 @@ describe('A server-side upload failure keeps the article unchanged and offers a
|
||||
])('shows the failed state for $name', async ({ trigger }) => {
|
||||
const wrapper = mountUploader()
|
||||
await openModal(wrapper)
|
||||
await chooseFile(fileFixture('north-wall.jpg', 'image/jpeg', 3.9))
|
||||
await chooseRasterFileAndConfirmUpload(fileFixture('north-wall.jpg', 'image/jpeg', 3.9))
|
||||
const xhr = latestXHR()
|
||||
|
||||
trigger(xhr)
|
||||
@@ -339,7 +355,7 @@ describe('"Try again" re-attempts the upload with the same file', () => {
|
||||
it('resends the same file under field "file"', async () => {
|
||||
const wrapper = mountUploader()
|
||||
await openModal(wrapper)
|
||||
await chooseFile(fileFixture('north-wall.jpg', 'image/jpeg', 3.9))
|
||||
await chooseRasterFileAndConfirmUpload(fileFixture('north-wall.jpg', 'image/jpeg', 3.9))
|
||||
latestXHR().resolve(500, '')
|
||||
await flushPromises()
|
||||
|
||||
@@ -359,7 +375,7 @@ describe('A server 400 for a type the client should have caught shows the same r
|
||||
vi.mocked(imageUpload.validateImageFile).mockReturnValueOnce({ ok: true })
|
||||
const wrapper = mountUploader()
|
||||
await openModal(wrapper)
|
||||
await chooseFile(fileFixture('notes.pdf', 'image/png', 1.2))
|
||||
await chooseRasterFileAndConfirmUpload(fileFixture('notes.pdf', 'image/png', 1.2))
|
||||
const xhr = latestXHR()
|
||||
|
||||
xhr.resolve(400, 'the file must be a PNG, JPEG, WEBP, or SVG image')
|
||||
@@ -380,7 +396,7 @@ describe('A server 400 for a size the client should have caught shows the size-n
|
||||
vi.mocked(imageUpload.validateImageFile).mockReturnValueOnce({ ok: true })
|
||||
const wrapper = mountUploader()
|
||||
await openModal(wrapper)
|
||||
await chooseFile(fileFixture('north-wall.tif', 'image/png', 14.2))
|
||||
await chooseRasterFileAndConfirmUpload(fileFixture('north-wall.tif', 'image/png', 14.2))
|
||||
const xhr = latestXHR()
|
||||
|
||||
xhr.resolve(400, 'the file exceeds the maximum upload size')
|
||||
|
||||
@@ -0,0 +1,429 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { defineComponent, nextTick, ref } from 'vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import uiPlugin from '@nuxt/ui/vue-plugin'
|
||||
import ImageUploader from '../ImageUploader.vue'
|
||||
import { renderCroppedImage } from '@/lib/imageCrop'
|
||||
import type { CropRect } from '@/lib/imageCrop'
|
||||
|
||||
vi.mock('@/lib/imageCrop', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/lib/imageCrop')>()
|
||||
return {
|
||||
...actual,
|
||||
readNaturalImageSize: vi.fn(async (file: File) => imageSizeFor(file)),
|
||||
renderCroppedImage: vi.fn(async () => new Blob([new Uint8Array(430080)])),
|
||||
}
|
||||
})
|
||||
|
||||
const imageSizes: Record<string, { width: number; height: number }> = {
|
||||
'north-wall.jpg': { width: 4032, height: 3024 },
|
||||
'sunset.jpg': { width: 3000, height: 2000 },
|
||||
}
|
||||
|
||||
function imageSizeFor(file: File) {
|
||||
return imageSizes[file.name] ?? { width: 4032, height: 3024 }
|
||||
}
|
||||
|
||||
class FakeXMLHttpRequest {
|
||||
static instances: FakeXMLHttpRequest[] = []
|
||||
|
||||
method = ''
|
||||
url = ''
|
||||
status = 0
|
||||
responseText = ''
|
||||
withCredentials = false
|
||||
sentBody: FormData | null = null
|
||||
|
||||
private listeners: Record<string, Array<(event?: unknown) => void>> = {}
|
||||
private uploadListeners: Record<string, (event: { loaded: number; total: number }) => void> = {}
|
||||
|
||||
upload = {
|
||||
addEventListener: (
|
||||
type: string,
|
||||
cb: (event: { loaded: number; total: number }) => void,
|
||||
) => {
|
||||
this.uploadListeners[type] = cb
|
||||
},
|
||||
}
|
||||
|
||||
constructor() {
|
||||
FakeXMLHttpRequest.instances.push(this)
|
||||
}
|
||||
|
||||
open(method: string, url: string) {
|
||||
this.method = method
|
||||
this.url = url
|
||||
}
|
||||
|
||||
send(body: FormData) {
|
||||
this.sentBody = body
|
||||
}
|
||||
|
||||
abort() {}
|
||||
|
||||
addEventListener(type: string, cb: (event?: unknown) => void) {
|
||||
this.listeners[type] = this.listeners[type] || []
|
||||
this.listeners[type].push(cb)
|
||||
}
|
||||
|
||||
dispatch(type: string, event?: unknown) {
|
||||
;(this.listeners[type] || []).forEach((cb) => cb(event))
|
||||
}
|
||||
|
||||
fireProgress(loaded: number, total: number) {
|
||||
this.uploadListeners.progress?.({ loaded, total })
|
||||
}
|
||||
|
||||
resolve(status: number, body: string) {
|
||||
this.status = status
|
||||
this.responseText = body
|
||||
this.dispatch('load')
|
||||
}
|
||||
}
|
||||
|
||||
function latestXHR() {
|
||||
return FakeXMLHttpRequest.instances[FakeXMLHttpRequest.instances.length - 1]
|
||||
}
|
||||
|
||||
function mib(value: number) {
|
||||
return Math.round(value * 1024 * 1024)
|
||||
}
|
||||
|
||||
function fileFixture(name: string, type: string, sizeMB: number) {
|
||||
const file = new File(['x'], name, { type })
|
||||
Object.defineProperty(file, 'size', { value: mib(sizeMB), configurable: true })
|
||||
return file
|
||||
}
|
||||
|
||||
const Host = defineComponent({
|
||||
components: { ImageUploader },
|
||||
props: {
|
||||
articleId: { type: Number, required: true },
|
||||
},
|
||||
emits: ['uploaded', 'close'],
|
||||
setup(_props, { emit }) {
|
||||
const open = ref(false)
|
||||
return {
|
||||
open,
|
||||
onUploaded: (location: string) => emit('uploaded', location),
|
||||
onClose: () => emit('close'),
|
||||
}
|
||||
},
|
||||
template: `<ImageUploader v-model:open="open" :article-id="articleId" @uploaded="onUploaded" @close="onClose" />`,
|
||||
})
|
||||
|
||||
function mountUploader() {
|
||||
return mount(Host, {
|
||||
props: { articleId: 12 },
|
||||
global: { plugins: [uiPlugin] },
|
||||
attachTo: document.body,
|
||||
})
|
||||
}
|
||||
|
||||
async function openModal(wrapper: ReturnType<typeof mountUploader>) {
|
||||
;(wrapper.vm as unknown as { open: boolean }).open = true
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
function fileInput() {
|
||||
return document.body.querySelector<HTMLInputElement>('[data-testid="image-file-input"]')!
|
||||
}
|
||||
|
||||
async function chooseFile(file: File) {
|
||||
const input = fileInput()
|
||||
Object.defineProperty(input, 'files', {
|
||||
value: { 0: file, length: 1 },
|
||||
configurable: true,
|
||||
})
|
||||
input.dispatchEvent(new Event('change'))
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
function findButton(text: string) {
|
||||
return Array.from(document.body.querySelectorAll('button')).find(
|
||||
(button) => button.textContent?.trim() === text,
|
||||
)
|
||||
}
|
||||
|
||||
async function clickButton(text: string) {
|
||||
findButton(text)?.click()
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
function radioPill(groupLabel: string, text: string) {
|
||||
const group = document.body.querySelector<HTMLElement>(
|
||||
`[role="radiogroup"][aria-label="${groupLabel}"]`,
|
||||
)!
|
||||
return Array.from(group.querySelectorAll<HTMLButtonElement>('[role="radio"]')).find(
|
||||
(pill) => pill.textContent?.trim() === text,
|
||||
)!
|
||||
}
|
||||
|
||||
async function selectRatioPill(text: string) {
|
||||
radioPill('Aspect ratio', text).click()
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
async function selectWidthPill(text: string) {
|
||||
radioPill('Width', text).click()
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
function cropFrame() {
|
||||
return document.body.querySelector<HTMLElement>('[data-testid="crop-frame"]')!
|
||||
}
|
||||
|
||||
function north_wall() {
|
||||
return fileFixture('north-wall.jpg', 'image/jpeg', 3.9)
|
||||
}
|
||||
|
||||
const RealXMLHttpRequest = global.XMLHttpRequest
|
||||
|
||||
beforeEach(() => {
|
||||
FakeXMLHttpRequest.instances = []
|
||||
global.XMLHttpRequest = FakeXMLHttpRequest as unknown as typeof XMLHttpRequest
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = ''
|
||||
global.XMLHttpRequest = RealXMLHttpRequest
|
||||
})
|
||||
|
||||
describe('A raster file opens the crop-and-resize step instead of uploading immediately', () => {
|
||||
it('opens "Crop and resize" with the opened defaults', async () => {
|
||||
const wrapper = mountUploader()
|
||||
await openModal(wrapper)
|
||||
|
||||
await chooseFile(north_wall())
|
||||
|
||||
expect(FakeXMLHttpRequest.instances).toHaveLength(0)
|
||||
expect(document.body.textContent).toContain('Crop and resize')
|
||||
expect(document.body.textContent).toContain('north-wall.jpg · 3.9 MB')
|
||||
expect(radioPill('Aspect ratio', 'Original').getAttribute('aria-checked')).toBe('true')
|
||||
expect(radioPill('Width', '1600 px').getAttribute('aria-checked')).toBe('true')
|
||||
expect(document.body.textContent).toContain('Uploads as 1600 × 1200 px · about 420 KB')
|
||||
expect(findButton('Choose another file')).toBeTruthy()
|
||||
expect(findButton('Upload image')).toBeTruthy()
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
describe('An SVG skips the crop step and uploads immediately', () => {
|
||||
it('sends the SVG right away', async () => {
|
||||
const wrapper = mountUploader()
|
||||
await openModal(wrapper)
|
||||
|
||||
await chooseFile(fileFixture('diagram.svg', 'image/svg+xml', 0.04))
|
||||
|
||||
expect(FakeXMLHttpRequest.instances).toHaveLength(1)
|
||||
expect(latestXHR().url).toBe('/v1/article/12/image')
|
||||
expect(document.body.textContent).not.toContain('Crop and resize')
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Choosing an aspect ratio locks the frame to it', () => {
|
||||
it.each([
|
||||
{ ratio: '16:9', cropWidth: 4032, cropHeight: 2268, outWidth: 1600, outHeight: 900 },
|
||||
{ ratio: '4:3', cropWidth: 4032, cropHeight: 3024, outWidth: 1600, outHeight: 1200 },
|
||||
{ ratio: '1:1', cropWidth: 3024, cropHeight: 3024, outWidth: 1600, outHeight: 1600 },
|
||||
])(
|
||||
'locks the frame for $ratio',
|
||||
async ({ ratio, cropWidth, cropHeight, outWidth, outHeight }) => {
|
||||
const wrapper = mountUploader()
|
||||
await openModal(wrapper)
|
||||
await chooseFile(north_wall())
|
||||
|
||||
await selectRatioPill(ratio)
|
||||
|
||||
const rect = (
|
||||
wrapper.findComponent(ImageUploader).vm as unknown as { cropRect: CropRect }
|
||||
).cropRect
|
||||
expect(rect.width).toBe(cropWidth)
|
||||
expect(rect.height).toBe(cropHeight)
|
||||
expect(document.body.textContent).toContain(
|
||||
`Uploads as ${outWidth} × ${outHeight} px · about`,
|
||||
)
|
||||
wrapper.unmount()
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
describe('Choosing a narrower width preset recomputes the output without upscaling', () => {
|
||||
it('shows a smaller estimate', async () => {
|
||||
const wrapper = mountUploader()
|
||||
await openModal(wrapper)
|
||||
await chooseFile(north_wall())
|
||||
|
||||
await selectWidthPill('800 px')
|
||||
|
||||
expect(document.body.textContent).toContain('Uploads as 800 × 600 px · about')
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
describe('A crop narrower than the chosen width is uploaded at its own size, not upscaled', () => {
|
||||
it('shows the info row and keeps the crop size', async () => {
|
||||
const wrapper = mountUploader()
|
||||
await openModal(wrapper)
|
||||
await chooseFile(north_wall())
|
||||
await selectRatioPill('1:1')
|
||||
|
||||
const uploader = wrapper.findComponent(ImageUploader).vm as unknown as {
|
||||
resizeCropTo: (w: number, h: number) => void
|
||||
}
|
||||
uploader.resizeCropTo(640, 640)
|
||||
await nextTick()
|
||||
|
||||
expect(document.body.textContent).toContain('Uploads as 640 × 640 px · about')
|
||||
expect(document.body.textContent).toContain(
|
||||
"The crop is 640 px wide, so it's uploaded at that size. Enlarging it would make it blurry.",
|
||||
)
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Free ratio removes the aspect-ratio constraint', () => {
|
||||
it('accepts an independent width and height', async () => {
|
||||
const wrapper = mountUploader()
|
||||
await openModal(wrapper)
|
||||
await chooseFile(north_wall())
|
||||
|
||||
await selectRatioPill('Free')
|
||||
const uploader = wrapper.findComponent(ImageUploader).vm as unknown as {
|
||||
resizeCropTo: (w: number, h: number) => void
|
||||
cropRect: CropRect
|
||||
}
|
||||
uploader.resizeCropTo(1000, 400)
|
||||
await nextTick()
|
||||
|
||||
expect(uploader.cropRect.width).toBe(1000)
|
||||
expect(uploader.cropRect.height).toBe(400)
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Arrow keys move the crop frame and Shift+arrow keys resize it', () => {
|
||||
it('moves 1px on ArrowRight and resizes 1px on Shift+ArrowRight', async () => {
|
||||
const wrapper = mountUploader()
|
||||
await openModal(wrapper)
|
||||
await chooseFile(north_wall())
|
||||
await selectRatioPill('1:1')
|
||||
const uploader = wrapper.findComponent(ImageUploader).vm as unknown as {
|
||||
cropRect: CropRect
|
||||
}
|
||||
const before = { ...uploader.cropRect }
|
||||
|
||||
cropFrame().dispatchEvent(
|
||||
new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }),
|
||||
)
|
||||
await nextTick()
|
||||
|
||||
expect(uploader.cropRect.x).toBe(before.x + 1)
|
||||
expect(uploader.cropRect.width).toBe(before.width)
|
||||
expect(document.body.textContent).toContain('Uploads as 1600 × 1600 px · about')
|
||||
|
||||
cropFrame().dispatchEvent(
|
||||
new KeyboardEvent('keydown', { key: 'ArrowRight', shiftKey: true, bubbles: true }),
|
||||
)
|
||||
await nextTick()
|
||||
|
||||
expect(uploader.cropRect.width).toBe(before.width + 1)
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Rotate cycles the image in 90° steps and Reset returns to the opened defaults', () => {
|
||||
it('rotates then resets', async () => {
|
||||
const wrapper = mountUploader()
|
||||
await openModal(wrapper)
|
||||
await chooseFile(north_wall())
|
||||
await selectRatioPill('16:9')
|
||||
await selectWidthPill('800 px')
|
||||
|
||||
await clickButton('Rotate')
|
||||
|
||||
expect(document.body.textContent).toContain('Uploads as')
|
||||
|
||||
await clickButton('Reset')
|
||||
|
||||
expect(radioPill('Aspect ratio', 'Original').getAttribute('aria-checked')).toBe('true')
|
||||
expect(radioPill('Width', '1600 px').getAttribute('aria-checked')).toBe('true')
|
||||
expect(document.body.textContent).toContain('Uploads as 1600 × 1200 px · about 420 KB')
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
describe('"Choose another file" returns to the file picker without uploading', () => {
|
||||
it('shows the idle state', async () => {
|
||||
const wrapper = mountUploader()
|
||||
await openModal(wrapper)
|
||||
await chooseFile(north_wall())
|
||||
|
||||
await clickButton('Choose another file')
|
||||
|
||||
expect(FakeXMLHttpRequest.instances).toHaveLength(0)
|
||||
expect(document.body.textContent).toContain('Drop an image here')
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
describe("A newly chosen file opens a fresh crop step, not the previous file's adjustments", () => {
|
||||
it('reopens at defaults for the new file', async () => {
|
||||
const wrapper = mountUploader()
|
||||
await openModal(wrapper)
|
||||
await chooseFile(north_wall())
|
||||
await selectRatioPill('16:9')
|
||||
await selectWidthPill('800 px')
|
||||
await clickButton('Choose another file')
|
||||
|
||||
await chooseFile(fileFixture('sunset.jpg', 'image/jpeg', 2.1))
|
||||
|
||||
expect(radioPill('Aspect ratio', 'Original').getAttribute('aria-checked')).toBe('true')
|
||||
expect(radioPill('Width', '1600 px').getAttribute('aria-checked')).toBe('true')
|
||||
const uploader = wrapper.findComponent(ImageUploader).vm as unknown as {
|
||||
cropRect: CropRect
|
||||
}
|
||||
expect(uploader.cropRect.width).toBe(3000)
|
||||
expect(uploader.cropRect.height).toBe(2000)
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
describe('"Upload image" uploads the cropped, resized file, not the original', () => {
|
||||
it('sends the resized file and shows resized progress', async () => {
|
||||
const wrapper = mountUploader()
|
||||
await openModal(wrapper)
|
||||
await chooseFile(north_wall())
|
||||
|
||||
await clickButton('Upload image')
|
||||
|
||||
expect(FakeXMLHttpRequest.instances).toHaveLength(1)
|
||||
const xhr = latestXHR()
|
||||
const sentFile = xhr.sentBody?.get('file') as File
|
||||
expect(sentFile.size).toBe(430080)
|
||||
expect(vi.mocked(renderCroppedImage)).toHaveBeenCalledWith(
|
||||
expect.any(File),
|
||||
expect.objectContaining({
|
||||
outputWidth: 1600,
|
||||
outputHeight: 1200,
|
||||
rect: expect.objectContaining({ width: 4032, height: 3024 }),
|
||||
}),
|
||||
)
|
||||
expect(document.body.textContent).toContain('Uploading 0.0 of 420 KB')
|
||||
|
||||
xhr.fireProgress(180 * 1024, 420 * 1024)
|
||||
await nextTick()
|
||||
|
||||
expect(document.body.textContent).toContain('north-wall.jpg 43%')
|
||||
expect(document.body.textContent).toContain('Uploading 180 of 420 KB')
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user