fix: focal and cropping issues, adds test (#4039)
This commit is contained in:
7
.vscode/launch.json
vendored
7
.vscode/launch.json
vendored
@@ -47,6 +47,13 @@
|
|||||||
"request": "launch",
|
"request": "launch",
|
||||||
"type": "node-terminal"
|
"type": "node-terminal"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"command": "pnpm run dev uploads",
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"name": "Run Dev Uploads",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "node-terminal"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"command": "PAYLOAD_BUNDLER=vite pnpm run dev fields",
|
"command": "PAYLOAD_BUNDLER=vite pnpm run dev fields",
|
||||||
"cwd": "${workspaceFolder}",
|
"cwd": "${workspaceFolder}",
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ export const EditUpload: React.FC<{
|
|||||||
setFormQueryParams({
|
setFormQueryParams({
|
||||||
...formQueryParams,
|
...formQueryParams,
|
||||||
uploadEdits: {
|
uploadEdits: {
|
||||||
crop: crop ? crop : undefined,
|
crop: crop || undefined,
|
||||||
focalPoint: pointPosition ? pointPosition : undefined,
|
focalPoint: pointPosition ? pointPosition : undefined,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ export const generateFileData = async <T>({
|
|||||||
let fileBuffer: { data: Buffer; info: OutputInfo }
|
let fileBuffer: { data: Buffer; info: OutputInfo }
|
||||||
let ext
|
let ext
|
||||||
let mime: string
|
let mime: string
|
||||||
const isSharpRequired =
|
const fileHasAdjustments =
|
||||||
fileSupportsResize &&
|
fileSupportsResize &&
|
||||||
Boolean(resizeOptions || formatOptions || trimOptions || file.tempFilePath)
|
Boolean(resizeOptions || formatOptions || trimOptions || file.tempFilePath)
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@ export const generateFileData = async <T>({
|
|||||||
|
|
||||||
if (fileIsAnimated) sharpOptions.animated = true
|
if (fileIsAnimated) sharpOptions.animated = true
|
||||||
|
|
||||||
if (isSharpRequired) {
|
if (fileHasAdjustments) {
|
||||||
if (file.tempFilePath) {
|
if (file.tempFilePath) {
|
||||||
sharpFile = sharp(file.tempFilePath, sharpOptions).rotate() // pass rotate() to auto-rotate based on EXIF data. https://github.com/payloadcms/payload/pull/3081
|
sharpFile = sharp(file.tempFilePath, sharpOptions).rotate() // pass rotate() to auto-rotate based on EXIF data. https://github.com/payloadcms/payload/pull/3081
|
||||||
} else {
|
} else {
|
||||||
@@ -174,7 +174,7 @@ export const generateFileData = async <T>({
|
|||||||
fileData.filename = fsSafeName
|
fileData.filename = fsSafeName
|
||||||
let fileForResize = file
|
let fileForResize = file
|
||||||
|
|
||||||
if (isSharpRequired && cropData) {
|
if (cropData) {
|
||||||
const { data: croppedImage, info } = await cropImage({ cropData, dimensions, file })
|
const { data: croppedImage, info } = await cropImage({ cropData, dimensions, file })
|
||||||
|
|
||||||
filesToSave.push({
|
filesToSave.push({
|
||||||
|
|||||||
@@ -113,36 +113,83 @@ const createResult = (
|
|||||||
*
|
*
|
||||||
* @param resizeConfig - object containing the requested dimensions and resize options
|
* @param resizeConfig - object containing the requested dimensions and resize options
|
||||||
* @param original - the original image size
|
* @param original - the original image size
|
||||||
* @returns true if the image needs to be resized, false otherwise
|
* @returns true if resizing is not needed, false otherwise
|
||||||
*/
|
*/
|
||||||
const needsResize = (
|
const preventResize = (
|
||||||
{ height: desiredHeight, width: desiredWidth, withoutEnlargement, withoutReduction }: ImageSize,
|
{ height: desiredHeight, width: desiredWidth, withoutEnlargement, withoutReduction }: ImageSize,
|
||||||
original: ProbedImageSize,
|
original: ProbedImageSize,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
// allow enlargement or prevent reduction (our default is to prevent
|
// default is to allow reduction
|
||||||
// enlargement and allow reduction)
|
if (withoutReduction !== undefined) {
|
||||||
if (withoutEnlargement !== undefined || withoutReduction !== undefined) {
|
return false // needs resize
|
||||||
return true // needs resize
|
}
|
||||||
|
|
||||||
|
// default is to prevent enlargement
|
||||||
|
if (withoutEnlargement !== undefined) {
|
||||||
|
return false // needs resize
|
||||||
}
|
}
|
||||||
|
|
||||||
const isWidthOrHeightNotDefined = !desiredHeight || !desiredWidth
|
const isWidthOrHeightNotDefined = !desiredHeight || !desiredWidth
|
||||||
|
|
||||||
if (isWidthOrHeightNotDefined) {
|
if (isWidthOrHeightNotDefined) {
|
||||||
// If with and height are not defined, it means there is a format conversion
|
// If with and height are not defined, it means there is a format conversion
|
||||||
// and the image needs to be "resized" (transformed).
|
// and the image needs to be "resized" (transformed).
|
||||||
return true // needs resize
|
return false // needs resize
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasInsufficientWidth = original.width < desiredWidth
|
const hasInsufficientWidth = desiredWidth > original.width
|
||||||
const hasInsufficientHeight = original.height < desiredHeight
|
const hasInsufficientHeight = desiredHeight > original.height
|
||||||
if (hasInsufficientWidth && hasInsufficientHeight) {
|
if (hasInsufficientWidth && hasInsufficientHeight) {
|
||||||
// doesn't need resize - prevent enlargement. This should only happen if both width and height are insufficient.
|
// doesn't need resize - prevent enlargement. This should only happen if both width and height are insufficient.
|
||||||
// if only one dimension is insufficient and the other is sufficient, resizing needs to happen, as the image
|
// if only one dimension is insufficient and the other is sufficient, resizing needs to happen, as the image
|
||||||
// should be resized to the sufficient dimension.
|
// should be resized to the sufficient dimension.
|
||||||
return false
|
return true // do not create a new size
|
||||||
}
|
}
|
||||||
|
|
||||||
return true // needs resize
|
return false // needs resize
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the image should be passed directly to sharp without payload adjusting properties.
|
||||||
|
*
|
||||||
|
* @param resizeConfig - object containing the requested dimensions and resize options
|
||||||
|
* @param original - the original image size
|
||||||
|
* @returns true if the image should passed directly to sharp
|
||||||
|
*/
|
||||||
|
const applyPayloadAdjustments = (
|
||||||
|
{ height, width, withoutEnlargement, withoutReduction }: ImageSize,
|
||||||
|
original: ProbedImageSize,
|
||||||
|
) => {
|
||||||
|
if (!isNumber(height) && !isNumber(width)) return false
|
||||||
|
|
||||||
|
const targetAspectRatio = width / height
|
||||||
|
const originalAspectRatio = original.width / original.height
|
||||||
|
if (originalAspectRatio === targetAspectRatio) return false
|
||||||
|
|
||||||
|
const skipEnlargement = withoutEnlargement && (original.height < height || original.width < width)
|
||||||
|
const skipReduction = withoutReduction && (original.height > height || original.width > width)
|
||||||
|
if (skipEnlargement || skipReduction) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitize the resize config. If the resize config has the `withoutReduction`
|
||||||
|
* property set to true, the `fit` and `position` properties will be set to `contain`
|
||||||
|
* and `top left` respectively.
|
||||||
|
*
|
||||||
|
* @param resizeConfig - the resize config
|
||||||
|
* @returns a sanitized resize config
|
||||||
|
*/
|
||||||
|
const sanitizeResizeConfig = (resizeConfig: ImageSize): ImageSize => {
|
||||||
|
if (resizeConfig.withoutReduction) {
|
||||||
|
return {
|
||||||
|
...resizeConfig,
|
||||||
|
// Why fit `contain` should also be set to https://github.com/lovell/sharp/issues/3595
|
||||||
|
fit: resizeConfig?.fit || 'contain',
|
||||||
|
position: resizeConfig?.position || 'left top',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resizeConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -176,66 +223,65 @@ export default async function resizeAndTransformImageSizes({
|
|||||||
|
|
||||||
const results: ImageSizesResult[] = await Promise.all(
|
const results: ImageSizesResult[] = await Promise.all(
|
||||||
imageSizes.map(async (imageResizeConfig): Promise<ImageSizesResult> => {
|
imageSizes.map(async (imageResizeConfig): Promise<ImageSizesResult> => {
|
||||||
|
imageResizeConfig = sanitizeResizeConfig(imageResizeConfig)
|
||||||
|
|
||||||
// This checks if a resize should happen. If not, the resized image will be
|
// This checks if a resize should happen. If not, the resized image will be
|
||||||
// skipped COMPLETELY and thus will not be included in the resulting images.
|
// skipped COMPLETELY and thus will not be included in the resulting images.
|
||||||
// All further format/trim options will thus be skipped as well.
|
// All further format/trim options will thus be skipped as well.
|
||||||
if (!needsResize(imageResizeConfig, dimensions)) {
|
if (preventResize(imageResizeConfig, dimensions)) {
|
||||||
return createResult(imageResizeConfig.name)
|
return createResult(imageResizeConfig.name)
|
||||||
}
|
}
|
||||||
let resized = sharpBase.clone()
|
|
||||||
|
|
||||||
const hasEdits = req.query?.uploadEdits
|
const imageToResize = sharpBase.clone()
|
||||||
|
let resized = imageToResize.resize(imageResizeConfig)
|
||||||
|
|
||||||
if (hasEdits && imageResizeConfig.width && imageResizeConfig.height) {
|
if (
|
||||||
const { height, width } = imageResizeConfig
|
req.query?.uploadEdits?.focalPoint &&
|
||||||
|
applyPayloadAdjustments(imageResizeConfig, dimensions)
|
||||||
const targetAspectRatio = width / height
|
) {
|
||||||
|
const { height: resizeHeight, width: resizeWidth } = imageResizeConfig
|
||||||
|
const resizeAspectRatio = resizeWidth / resizeHeight
|
||||||
const originalAspectRatio = dimensions.width / dimensions.height
|
const originalAspectRatio = dimensions.width / dimensions.height
|
||||||
|
const prioritizeHeight = resizeAspectRatio < originalAspectRatio
|
||||||
|
|
||||||
if (originalAspectRatio === targetAspectRatio) {
|
// Scale the image up or down to fit the resize dimensions
|
||||||
resized = resized.resize(imageResizeConfig)
|
const scaledImage = imageToResize.resize({
|
||||||
} else {
|
height: prioritizeHeight ? resizeHeight : null,
|
||||||
const focalPoint = {
|
width: prioritizeHeight ? null : resizeWidth,
|
||||||
x: 0.5,
|
})
|
||||||
y: 0.5,
|
const { info: scaledImageInfo } = await scaledImage.toBuffer({ resolveWithObject: true })
|
||||||
}
|
|
||||||
|
|
||||||
if (req.query.uploadEdits?.focalPoint) {
|
// Focal point adjustments
|
||||||
if (isNumber(req.query.uploadEdits.focalPoint?.x)) {
|
const focalPoint = {
|
||||||
focalPoint.x = req.query.uploadEdits.focalPoint.x
|
x: isNumber(req.query.uploadEdits.focalPoint?.x)
|
||||||
}
|
? req.query.uploadEdits.focalPoint.x
|
||||||
if (isNumber(req.query.uploadEdits.focalPoint?.y)) {
|
: 50,
|
||||||
focalPoint.y = req.query.uploadEdits.focalPoint.y
|
y: isNumber(req.query.uploadEdits.focalPoint?.y)
|
||||||
}
|
? req.query.uploadEdits.focalPoint.y
|
||||||
}
|
: 50,
|
||||||
|
|
||||||
const prioritizeHeight = originalAspectRatio > targetAspectRatio
|
|
||||||
|
|
||||||
const { info } = await resized
|
|
||||||
.resize({
|
|
||||||
height: prioritizeHeight ? height : null,
|
|
||||||
width: prioritizeHeight ? null : width,
|
|
||||||
})
|
|
||||||
.toBuffer({ resolveWithObject: true })
|
|
||||||
|
|
||||||
const maxOffsetX = Math.max(info.width - width, 0)
|
|
||||||
const maxOffsetY = Math.max(info.height - height, 0)
|
|
||||||
|
|
||||||
const focalPointX = Math.floor((info.width / 100) * focalPoint.x)
|
|
||||||
const focalPointY = Math.floor((info.height / 100) * focalPoint.y)
|
|
||||||
|
|
||||||
const offsetX = Math.min(Math.max(focalPointX - width / 2, 0), maxOffsetX)
|
|
||||||
const offsetY = Math.min(Math.max(focalPointY - height / 2, 0), maxOffsetY)
|
|
||||||
|
|
||||||
resized = resized.extract({
|
|
||||||
height,
|
|
||||||
left: offsetX,
|
|
||||||
top: offsetY,
|
|
||||||
width,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
resized = resized.resize(imageResizeConfig)
|
const safeResizeWidth = resizeWidth ?? scaledImageInfo.width
|
||||||
|
const maxOffsetX = scaledImageInfo.width - safeResizeWidth
|
||||||
|
const leftFocalEdge = Math.round(
|
||||||
|
scaledImageInfo.width * (focalPoint.x / 100) - safeResizeWidth / 2,
|
||||||
|
)
|
||||||
|
const safeOffsetX = Math.min(Math.max(0, leftFocalEdge), maxOffsetX)
|
||||||
|
|
||||||
|
const safeResizeHeight = resizeHeight ?? scaledImageInfo.height
|
||||||
|
const maxOffsetY = scaledImageInfo.height - safeResizeHeight
|
||||||
|
const topFocalEdge = Math.round(
|
||||||
|
scaledImageInfo.height * (focalPoint.y / 100) - safeResizeHeight / 2,
|
||||||
|
)
|
||||||
|
const safeOffsetY = Math.min(Math.max(0, topFocalEdge), maxOffsetY)
|
||||||
|
|
||||||
|
// extract the focal area from the scaled image
|
||||||
|
resized = scaledImage.extract({
|
||||||
|
height: safeResizeHeight,
|
||||||
|
left: safeOffsetX,
|
||||||
|
top: safeOffsetY,
|
||||||
|
width: safeResizeWidth,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (imageResizeConfig.formatOptions) {
|
if (imageResizeConfig.formatOptions) {
|
||||||
|
|||||||
@@ -330,7 +330,6 @@ export default buildConfigWithDefaults({
|
|||||||
height: 80,
|
height: 80,
|
||||||
formatOptions: { format: 'jpg', options: { quality: 90 } },
|
formatOptions: { format: 'jpg', options: { quality: 90 } },
|
||||||
withoutReduction: true,
|
withoutReduction: true,
|
||||||
fit: 'contain',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'resizedLarger',
|
name: 'resizedLarger',
|
||||||
@@ -341,9 +340,7 @@ export default buildConfigWithDefaults({
|
|||||||
name: 'resizedSmaller',
|
name: 'resizedSmaller',
|
||||||
width: 180,
|
width: 180,
|
||||||
height: 50,
|
height: 50,
|
||||||
// Why fit `contain` should also be set to https://github.com/lovell/sharp/issues/3595
|
|
||||||
withoutReduction: true,
|
withoutReduction: true,
|
||||||
fit: 'contain',
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,11 +10,13 @@ import wait from '../../packages/payload/src/utilities/wait'
|
|||||||
import { saveDocAndAssert } from '../helpers'
|
import { saveDocAndAssert } from '../helpers'
|
||||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil'
|
import { AdminUrlUtil } from '../helpers/adminUrlUtil'
|
||||||
import { initPayloadE2E } from '../helpers/configHelpers'
|
import { initPayloadE2E } from '../helpers/configHelpers'
|
||||||
|
import { RESTClient } from '../helpers/rest'
|
||||||
import { adminThumbnailSrc } from './collections/admin-thumbnail'
|
import { adminThumbnailSrc } from './collections/admin-thumbnail'
|
||||||
import { adminThumbnailSlug, audioSlug, mediaSlug, relationSlug } from './shared'
|
import { adminThumbnailSlug, audioSlug, mediaSlug, relationSlug } from './shared'
|
||||||
|
|
||||||
const { beforeAll, describe } = test
|
const { beforeAll, describe } = test
|
||||||
|
|
||||||
|
let client: RESTClient
|
||||||
let mediaURL: AdminUrlUtil
|
let mediaURL: AdminUrlUtil
|
||||||
let audioURL: AdminUrlUtil
|
let audioURL: AdminUrlUtil
|
||||||
let relationURL: AdminUrlUtil
|
let relationURL: AdminUrlUtil
|
||||||
@@ -27,6 +29,8 @@ describe('uploads', () => {
|
|||||||
|
|
||||||
beforeAll(async ({ browser }) => {
|
beforeAll(async ({ browser }) => {
|
||||||
const { serverURL } = await initPayloadE2E(__dirname)
|
const { serverURL } = await initPayloadE2E(__dirname)
|
||||||
|
client = new RESTClient(null, { serverURL, defaultSlug: 'users' })
|
||||||
|
await client.login()
|
||||||
|
|
||||||
mediaURL = new AdminUrlUtil(serverURL, mediaSlug)
|
mediaURL = new AdminUrlUtil(serverURL, mediaSlug)
|
||||||
audioURL = new AdminUrlUtil(serverURL, audioSlug)
|
audioURL = new AdminUrlUtil(serverURL, audioSlug)
|
||||||
@@ -47,7 +51,7 @@ describe('uploads', () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
pngDoc = findPNG.docs[0] as Media
|
pngDoc = findPNG.docs[0] as unknown as Media
|
||||||
|
|
||||||
const findAudio = await payload.find({
|
const findAudio = await payload.find({
|
||||||
collection: audioSlug,
|
collection: audioSlug,
|
||||||
@@ -55,7 +59,7 @@ describe('uploads', () => {
|
|||||||
pagination: false,
|
pagination: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
audioDoc = findAudio.docs[0] as Media
|
audioDoc = findAudio.docs[0] as unknown as Media
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should see upload filename in relation list', async () => {
|
test('should see upload filename in relation list', async () => {
|
||||||
@@ -88,10 +92,6 @@ describe('uploads', () => {
|
|||||||
await saveDocAndAssert(page)
|
await saveDocAndAssert(page)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should update file upload', async () => {
|
|
||||||
await page.goto(mediaURL.edit(pngDoc.id))
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should show resized images', async () => {
|
test('should show resized images', async () => {
|
||||||
await page.goto(mediaURL.edit(pngDoc.id))
|
await page.goto(mediaURL.edit(pngDoc.id))
|
||||||
|
|
||||||
@@ -192,4 +192,79 @@ describe('uploads', () => {
|
|||||||
const audioUploadImage = page.locator('tr.row-2 .thumbnail img')
|
const audioUploadImage = page.locator('tr.row-2 .thumbnail img')
|
||||||
expect(await audioUploadImage.getAttribute('src')).toContain(adminThumbnailSrc)
|
expect(await audioUploadImage.getAttribute('src')).toContain(adminThumbnailSrc)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('image manipulation', () => {
|
||||||
|
test('should crop image correctly', async () => {
|
||||||
|
const positions = {
|
||||||
|
'top-left': {
|
||||||
|
focalX: 25,
|
||||||
|
focalY: 25,
|
||||||
|
dragX: 0,
|
||||||
|
dragY: 0,
|
||||||
|
},
|
||||||
|
'bottom-right': {
|
||||||
|
focalX: 75,
|
||||||
|
focalY: 75,
|
||||||
|
dragX: 800,
|
||||||
|
dragY: 800,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const createFocalCrop = async (page: Page, position: 'bottom-right' | 'top-left') => {
|
||||||
|
const { focalX, focalY, dragX, dragY } = positions[position]
|
||||||
|
await page.goto(mediaURL.create)
|
||||||
|
|
||||||
|
// select and upload file
|
||||||
|
const fileChooserPromise = page.waitForEvent('filechooser')
|
||||||
|
await page.getByText('Select a file').click()
|
||||||
|
const fileChooser = await fileChooserPromise
|
||||||
|
await fileChooser.setFiles(path.join(__dirname, 'test-image.jpg'))
|
||||||
|
await page.locator('.file-field__edit').click()
|
||||||
|
|
||||||
|
// set crop
|
||||||
|
await page.locator('.edit-upload__input input[name="Width (px)"]').fill('400')
|
||||||
|
await page.locator('.edit-upload__input input[name="Height (px)"]').fill('400')
|
||||||
|
// set focal point
|
||||||
|
await page.locator('.edit-upload__input input[name="X %"]').fill('25') // init left focal point
|
||||||
|
await page.locator('.edit-upload__input input[name="Y %"]').fill('25') // init top focal point
|
||||||
|
|
||||||
|
// hover the crop selection, position mouse outside of focal point hitbox
|
||||||
|
await page.locator('.ReactCrop__crop-selection').hover({ position: { x: 100, y: 100 } })
|
||||||
|
await page.mouse.down() // start drag
|
||||||
|
await page.mouse.move(dragX, dragY) // drag selection to the lower right corner
|
||||||
|
await page.mouse.up() // release drag
|
||||||
|
|
||||||
|
// focal point should reset to center
|
||||||
|
await expect(page.locator('.edit-upload__input input[name="X %"]')).toHaveValue(`${focalX}`)
|
||||||
|
await expect(page.locator('.edit-upload__input input[name="Y %"]')).toHaveValue(`${focalY}`)
|
||||||
|
|
||||||
|
await page.locator('button:has-text("Apply Changes")').click()
|
||||||
|
await page.waitForSelector('button#action-save')
|
||||||
|
await page.locator('button#action-save').click()
|
||||||
|
}
|
||||||
|
|
||||||
|
await createFocalCrop(page, 'bottom-right') // green square
|
||||||
|
await wait(1000) // wait for edit view navigation (saving images)
|
||||||
|
// get the ID of the doc
|
||||||
|
const greenSquareMediaID = page.url().split('/').pop()
|
||||||
|
await createFocalCrop(page, 'top-left') // red square
|
||||||
|
await wait(1000) // wait for edit view navigation (saving images)
|
||||||
|
const redSquareMediaID = page.url().split('/').pop()
|
||||||
|
|
||||||
|
const { doc: greenDoc } = await client.findByID({
|
||||||
|
id: greenSquareMediaID,
|
||||||
|
slug: mediaSlug,
|
||||||
|
auth: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { doc: redDoc } = await client.findByID({
|
||||||
|
id: redSquareMediaID,
|
||||||
|
slug: mediaSlug,
|
||||||
|
auth: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// green and red squares should have different sizes (colors make the difference)
|
||||||
|
expect(greenDoc.filesize).toEqual(1205)
|
||||||
|
expect(redDoc.filesize).toEqual(1207)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
BIN
test/uploads/test-image.jpg
Normal file
BIN
test/uploads/test-image.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
Reference in New Issue
Block a user