**BREAKING:** We now export toast from `sonner` instead of `react-toastify`. If you send out toasts from your own projects, make sure to use our `toast` export, or install `sonner`. React-toastify toasts will no longer work anymore. The Toast APIs are mostly similar, but there are some differences if you provide options to your toast CSS styles have been changed from Toastify ```css /* before */ .Toastify /* current */ .payload-toast-container .payload-toast-item .payload-toast-close-button /* individual toast items will also have these classes depending on the state */ .toast-info .toast-warning .toast-success .toast-error ``` https://github.com/payloadcms/payload/assets/70709113/da3e732e-aafc-4008-9469-b10f4eb06b35 --------- Co-authored-by: Paul Popus <paul@nouance.io>
381 lines
13 KiB
TypeScript
381 lines
13 KiB
TypeScript
import type { Page } from '@playwright/test'
|
|
import type { Payload } from 'payload/types'
|
|
|
|
import { expect, test } from '@playwright/test'
|
|
import path from 'path'
|
|
import { wait } from 'payload/utilities'
|
|
import { fileURLToPath } from 'url'
|
|
|
|
import type { PayloadTestSDK } from '../helpers/sdk/index.js'
|
|
import type { Config, Media } from './payload-types.js'
|
|
|
|
import {
|
|
ensureAutoLoginAndCompilationIsDone,
|
|
initPageConsoleErrorCatch,
|
|
openDocDrawer,
|
|
saveDocAndAssert,
|
|
} from '../helpers.js'
|
|
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
|
|
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
|
|
import { RESTClient } from '../helpers/rest.js'
|
|
import { TEST_TIMEOUT_LONG } from '../playwright.config.js'
|
|
import {
|
|
adminThumbnailFunctionSlug,
|
|
adminThumbnailSizeSlug,
|
|
audioSlug,
|
|
mediaSlug,
|
|
relationSlug,
|
|
} from './shared.js'
|
|
const filename = fileURLToPath(import.meta.url)
|
|
const dirname = path.dirname(filename)
|
|
|
|
const { beforeAll, describe } = test
|
|
|
|
let payload: PayloadTestSDK<Config>
|
|
let client: RESTClient
|
|
let serverURL: string
|
|
let mediaURL: AdminUrlUtil
|
|
let audioURL: AdminUrlUtil
|
|
let relationURL: AdminUrlUtil
|
|
let adminThumbnailSizeURL: AdminUrlUtil
|
|
let adminThumbnailFunctionURL: AdminUrlUtil
|
|
|
|
describe('uploads', () => {
|
|
let page: Page
|
|
let pngDoc: Media
|
|
let audioDoc: Media
|
|
|
|
beforeAll(async ({ browser }, testInfo) => {
|
|
testInfo.setTimeout(TEST_TIMEOUT_LONG)
|
|
;({ payload, serverURL } = await initPayloadE2ENoConfig<Config>({ dirname }))
|
|
client = new RESTClient(null, { defaultSlug: 'users', serverURL })
|
|
await client.login()
|
|
|
|
mediaURL = new AdminUrlUtil(serverURL, mediaSlug)
|
|
audioURL = new AdminUrlUtil(serverURL, audioSlug)
|
|
relationURL = new AdminUrlUtil(serverURL, relationSlug)
|
|
adminThumbnailSizeURL = new AdminUrlUtil(serverURL, adminThumbnailSizeSlug)
|
|
adminThumbnailFunctionURL = new AdminUrlUtil(serverURL, adminThumbnailFunctionSlug)
|
|
|
|
const context = await browser.newContext()
|
|
page = await context.newPage()
|
|
|
|
initPageConsoleErrorCatch(page)
|
|
|
|
const findPNG = await payload.find({
|
|
collection: mediaSlug,
|
|
depth: 0,
|
|
pagination: false,
|
|
where: {
|
|
mimeType: {
|
|
equals: 'image/png',
|
|
},
|
|
},
|
|
})
|
|
|
|
pngDoc = findPNG.docs[0] as unknown as Media
|
|
|
|
const findAudio = await payload.find({
|
|
collection: audioSlug,
|
|
depth: 0,
|
|
pagination: false,
|
|
})
|
|
|
|
audioDoc = findAudio.docs[0] as unknown as Media
|
|
|
|
await ensureAutoLoginAndCompilationIsDone({ page, serverURL })
|
|
})
|
|
|
|
test('should see upload filename in relation list', async () => {
|
|
await page.goto(relationURL.list)
|
|
const field = page.locator('.cell-image')
|
|
|
|
await expect(field).toContainText('image.png')
|
|
})
|
|
|
|
test('should see upload versioned filename in relation list', async () => {
|
|
await page.goto(relationURL.list)
|
|
const field = page.locator('.cell-versionedImage')
|
|
|
|
await expect(field).toContainText('image')
|
|
})
|
|
|
|
test('should show upload filename in upload collection list', async () => {
|
|
await page.goto(mediaURL.list)
|
|
const audioUpload = page.locator('tr.row-1 .cell-filename')
|
|
await expect(audioUpload).toHaveText('audio.mp3')
|
|
|
|
const imageUpload = page.locator('tr.row-2 .cell-filename')
|
|
await expect(imageUpload).toHaveText('image.png')
|
|
})
|
|
|
|
test('should create file upload', async () => {
|
|
await page.goto(mediaURL.create)
|
|
await page.setInputFiles('input[type="file"]', path.resolve(dirname, './image.png'))
|
|
|
|
const filename = page.locator('.file-field__filename')
|
|
|
|
await expect(filename).toHaveValue('image.png')
|
|
|
|
await saveDocAndAssert(page)
|
|
})
|
|
|
|
test('should show resized images', async () => {
|
|
await page.goto(mediaURL.edit(pngDoc.id))
|
|
|
|
await page.locator('.file-field__previewSizes').click()
|
|
|
|
const maintainedAspectRatioItem = page
|
|
.locator('.preview-sizes__list .preview-sizes__sizeOption')
|
|
.nth(1)
|
|
.locator('.file-meta__size-type')
|
|
await expect(maintainedAspectRatioItem).toContainText('1024x1024')
|
|
|
|
const differentFormatFromMainImageMeta = page
|
|
.locator('.preview-sizes__list .preview-sizes__sizeOption')
|
|
.nth(2)
|
|
.locator('.file-meta__size-type')
|
|
await expect(differentFormatFromMainImageMeta).toContainText('image/jpeg')
|
|
|
|
const maintainedImageSizeMeta = page
|
|
.locator('.preview-sizes__list .preview-sizes__sizeOption')
|
|
.nth(3)
|
|
.locator('.file-meta__size-type')
|
|
await expect(maintainedImageSizeMeta).toContainText('1600x1600')
|
|
|
|
const maintainedImageSizeWithNewFormatMeta = page
|
|
.locator('.preview-sizes__list .preview-sizes__sizeOption')
|
|
.nth(4)
|
|
.locator('.file-meta__size-type')
|
|
await expect(maintainedImageSizeWithNewFormatMeta).toContainText('1600x1600')
|
|
await expect(maintainedImageSizeWithNewFormatMeta).toContainText('image/jpeg')
|
|
|
|
const sameSizeMeta = page
|
|
.locator('.preview-sizes__list .preview-sizes__sizeOption')
|
|
.nth(5)
|
|
.locator('.file-meta__size-type')
|
|
await expect(sameSizeMeta).toContainText('320x80')
|
|
|
|
const tabletMeta = page
|
|
.locator('.preview-sizes__list .preview-sizes__sizeOption')
|
|
.nth(6)
|
|
.locator('.file-meta__size-type')
|
|
await expect(tabletMeta).toContainText('640x480')
|
|
|
|
const mobileMeta = page
|
|
.locator('.preview-sizes__list .preview-sizes__sizeOption')
|
|
.nth(7)
|
|
.locator('.file-meta__size-type')
|
|
await expect(mobileMeta).toContainText('320x240')
|
|
|
|
const iconMeta = page
|
|
.locator('.preview-sizes__list .preview-sizes__sizeOption')
|
|
.nth(8)
|
|
.locator('.file-meta__size-type')
|
|
await expect(iconMeta).toContainText('16x16')
|
|
})
|
|
|
|
test('should resize and show tiff images', async () => {
|
|
await page.goto(mediaURL.create)
|
|
await page.setInputFiles('input[type="file"]', path.resolve(dirname, './test-image.tiff'))
|
|
|
|
await expect(page.locator('.file-field__upload .thumbnail svg')).toBeVisible()
|
|
|
|
await saveDocAndAssert(page)
|
|
|
|
await expect(page.locator('.file-details img')).toBeVisible()
|
|
})
|
|
|
|
test('should show draft uploads in the relation list', async () => {
|
|
await page.goto(relationURL.list)
|
|
// from the list edit the first document
|
|
await page.locator('.row-1 a').click()
|
|
|
|
// edit the versioned image
|
|
await page.locator('.field-type:nth-of-type(2) .icon--edit').click()
|
|
|
|
// fill the title with 'draft'
|
|
await page.locator('#field-title').fill('draft')
|
|
|
|
// save draft
|
|
await page.locator('#action-save-draft').click()
|
|
|
|
// close the drawer
|
|
await page.locator('.doc-drawer__header-close').click()
|
|
|
|
// remove the selected versioned image
|
|
await page.locator('.field-type:nth-of-type(2) .icon--x').click()
|
|
|
|
// choose from existing
|
|
await openDocDrawer(page, '.list-drawer__toggler')
|
|
|
|
await expect(page.locator('.cell-title')).toContainText('draft')
|
|
})
|
|
|
|
test('should restrict mimetype based on filterOptions', async () => {
|
|
await page.goto(audioURL.edit(audioDoc.id))
|
|
await page.waitForURL(audioURL.edit(audioDoc.id))
|
|
|
|
// remove the selection and open the list drawer
|
|
await wait(500) // flake workaround
|
|
await page.locator('.file-details__remove').click()
|
|
|
|
await openDocDrawer(page, '.upload__toggler.list-drawer__toggler')
|
|
|
|
const listDrawer = page.locator('[id^=list-drawer_1_]')
|
|
await expect(listDrawer).toBeVisible()
|
|
|
|
await openDocDrawer(page, 'button.list-drawer__create-new-button.doc-drawer__toggler')
|
|
await expect(page.locator('[id^=doc-drawer_media_2_]')).toBeVisible()
|
|
|
|
// upload an image and try to select it
|
|
await page
|
|
.locator('[id^=doc-drawer_media_2_] .file-field__upload input[type="file"]')
|
|
.setInputFiles(path.resolve(dirname, './image.png'))
|
|
await page.locator('[id^=doc-drawer_media_2_] button#action-save').click()
|
|
await expect(page.locator('.payload-toast-container .toast-success')).toContainText(
|
|
'successfully',
|
|
)
|
|
await page
|
|
.locator('.payload-toast-container .toast-success .payload-toast-close-button')
|
|
.click()
|
|
|
|
// save the document and expect an error
|
|
await page.locator('button#action-save').click()
|
|
await expect(page.locator('.payload-toast-container .toast-error')).toContainText(
|
|
'The following field is invalid: audio',
|
|
)
|
|
})
|
|
|
|
test('should throw error when file is larger than the limit and abortOnLimit is true', async () => {
|
|
await page.goto(mediaURL.create)
|
|
await page.setInputFiles('input[type="file"]', path.resolve(dirname, './2mb.jpg'))
|
|
await expect(page.locator('.file-field__filename')).toHaveValue('2mb.jpg')
|
|
|
|
await page.click('#action-save', { delay: 100 })
|
|
await expect(page.locator('.payload-toast-container .toast-error')).toContainText(
|
|
'File size limit has been reached',
|
|
)
|
|
})
|
|
|
|
test('should render adminThumbnail when using a function', async () => {
|
|
await page.reload() // Flakey test, it likely has to do with the test that comes before it. Trace viewer is not helpful when it fails.
|
|
await page.goto(adminThumbnailFunctionURL.list)
|
|
await page.waitForURL(adminThumbnailFunctionURL.list)
|
|
|
|
// Ensure sure false or null shows generic file svg
|
|
const genericUploadImage = page.locator('tr.row-1 .thumbnail img')
|
|
await expect(genericUploadImage).toHaveAttribute(
|
|
'src',
|
|
'https://payloadcms.com/images/universal-truth.jpg',
|
|
)
|
|
})
|
|
|
|
test('should render adminThumbnail when using a specific size', async () => {
|
|
await page.goto(adminThumbnailSizeURL.list)
|
|
await page.waitForURL(adminThumbnailSizeURL.list)
|
|
|
|
// Ensure sure false or null shows generic file svg
|
|
const genericUploadImage = page.locator('tr.row-1 .thumbnail img')
|
|
await expect(genericUploadImage).toBeVisible()
|
|
|
|
// Ensure adminThumbnail fn returns correct value based on audio/mp3 mime
|
|
const audioUploadImage = page.locator('tr.row-2 .thumbnail svg')
|
|
await expect(audioUploadImage).toBeVisible()
|
|
})
|
|
|
|
test('should detect correct mimeType', async () => {
|
|
await page.goto(mediaURL.create)
|
|
await page.waitForURL(mediaURL.create)
|
|
await page.setInputFiles('input[type="file"]', path.resolve(dirname, './image.png'))
|
|
await saveDocAndAssert(page)
|
|
|
|
const imageID = page.url().split('/').pop()
|
|
|
|
const { doc: uploadedImage } = await client.findByID({
|
|
id: imageID,
|
|
slug: mediaSlug,
|
|
auth: true,
|
|
})
|
|
|
|
expect(uploadedImage.mimeType).toEqual('image/png')
|
|
})
|
|
|
|
describe('image manipulation', () => {
|
|
test('should crop image correctly', async () => {
|
|
const positions = {
|
|
'bottom-right': {
|
|
dragX: 800,
|
|
dragY: 800,
|
|
focalX: 75,
|
|
focalY: 75,
|
|
},
|
|
'top-left': {
|
|
dragX: 0,
|
|
dragY: 0,
|
|
focalX: 25,
|
|
focalY: 25,
|
|
},
|
|
}
|
|
const createFocalCrop = async (page: Page, position: 'bottom-right' | 'top-left') => {
|
|
const { dragX, dragY, focalX, focalY } = positions[position]
|
|
await page.goto(mediaURL.create)
|
|
await page.waitForURL(mediaURL.create)
|
|
// select and upload file
|
|
const fileChooserPromise = page.waitForEvent('filechooser')
|
|
await page.getByText('Select a file').click()
|
|
const fileChooser = await fileChooserPromise
|
|
await wait(1000)
|
|
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}`)
|
|
|
|
// apply crop
|
|
await page.locator('button:has-text("Apply Changes")').click()
|
|
await page.waitForSelector('button#action-save')
|
|
await page.locator('button#action-save').click()
|
|
await expect(page.locator('.payload-toast-container')).toContainText('successfully')
|
|
await wait(1000) // Wait for the save
|
|
}
|
|
|
|
await createFocalCrop(page, 'bottom-right') // green square
|
|
const greenSquareMediaID = page.url().split('/').pop() // get the ID of the doc
|
|
await createFocalCrop(page, 'top-left') // red square
|
|
const redSquareMediaID = page.url().split('/').pop() // get the ID of the doc
|
|
|
|
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)
|
|
})
|
|
})
|
|
})
|