Files
payloadcms/test/admin/e2e/document-view/e2e.spec.ts
Jacob Fletcher b975858e76 test: removes all unnecessary page.waitForURL methods (#11412)
Removes all unnecessary `page.waitForURL` methods within e2e tests.
These are unneeded when following a `page.goto` call because the
subsequent page load is already being awaited.

It is only a requirement when:

- Clicking a link and expecting navigation
- Expecting a redirect after a route change
- Waiting for a change in search params
2025-02-26 16:54:39 -05:00

552 lines
20 KiB
TypeScript

import type { Page } from '@playwright/test'
import { expect, test } from '@playwright/test'
import { wait } from 'payload/shared'
import type { Config, Post } from '../../payload-types.js'
import {
checkBreadcrumb,
checkPageTitle,
ensureCompilationIsDone,
exactText,
initPageConsoleErrorCatch,
openNav,
saveDocAndAssert,
} from '../../../helpers.js'
import { AdminUrlUtil } from '../../../helpers/adminUrlUtil.js'
import { initPayloadE2ENoConfig } from '../../../helpers/initPayloadE2ENoConfig.js'
import {
customAdminRoutes,
customEditLabel,
customNestedTabViewPath,
customNestedTabViewTitle,
customTabAdminDescription,
customTabLabel,
customTabViewPath,
customTabViewTitle,
} from '../../shared.js'
import {
customFieldsSlug,
customGlobalViews2GlobalSlug,
customViews2CollectionSlug,
globalSlug,
group1Collection1Slug,
group1GlobalSlug,
noApiViewCollectionSlug,
noApiViewGlobalSlug,
postsCollectionSlug,
} from '../../slugs.js'
const { beforeAll, beforeEach, describe } = test
const title = 'Title'
const description = 'Description'
let payload: PayloadTestSDK<Config>
import { navigateToDoc } from 'helpers/e2e/navigateToDoc.js'
import path from 'path'
import { fileURLToPath } from 'url'
import type { PayloadTestSDK } from '../../../helpers/sdk/index.js'
import { reInitializeDB } from '../../../helpers/reInitializeDB.js'
import { TEST_TIMEOUT_LONG } from '../../../playwright.config.js'
const filename = fileURLToPath(import.meta.url)
const currentFolder = path.dirname(filename)
const dirname = path.resolve(currentFolder, '../../')
describe('Document View', () => {
let page: Page
let postsUrl: AdminUrlUtil
let globalURL: AdminUrlUtil
let serverURL: string
let customViewsURL: AdminUrlUtil
let customFieldsURL: AdminUrlUtil
beforeAll(async ({ browser }, testInfo) => {
const prebuild = false // Boolean(process.env.CI)
testInfo.setTimeout(TEST_TIMEOUT_LONG)
process.env.SEED_IN_CONFIG_ONINIT = 'false' // Makes it so the payload config onInit seed is not run. Otherwise, the seed would be run unnecessarily twice for the initial test run - once for beforeEach and once for onInit
;({ payload, serverURL } = await initPayloadE2ENoConfig<Config>({
dirname,
prebuild,
}))
postsUrl = new AdminUrlUtil(serverURL, postsCollectionSlug)
globalURL = new AdminUrlUtil(serverURL, globalSlug)
customViewsURL = new AdminUrlUtil(serverURL, customViews2CollectionSlug)
customFieldsURL = new AdminUrlUtil(serverURL, customFieldsSlug)
const context = await browser.newContext()
page = await context.newPage()
initPageConsoleErrorCatch(page)
await ensureCompilationIsDone({ customAdminRoutes, page, serverURL })
})
beforeEach(async () => {
await reInitializeDB({
serverURL,
snapshotKey: 'adminTests',
})
await ensureCompilationIsDone({ customAdminRoutes, page, serverURL })
})
describe('API view', () => {
test('collection — should not show API tab when disabled in config', async () => {
await page.goto(postsUrl.collection(noApiViewCollectionSlug))
await page.locator('.collection-list .table a').click()
await expect(page.locator('.doc-tabs__tabs-container')).not.toContainText('API')
})
test('collection — should not enable API route when disabled in config', async () => {
const collectionItems = await payload.find({
collection: noApiViewCollectionSlug,
limit: 1,
})
expect(collectionItems.docs.length).toBe(1)
await page.goto(
`${postsUrl.collection(noApiViewGlobalSlug)}/${collectionItems.docs[0].id}/api`,
)
await expect(page.locator('.not-found')).toHaveCount(1)
})
test('collection — sidebar fields should respond to permission', async () => {
const { id } = await createPost()
await page.goto(postsUrl.edit(id))
await expect(page.locator('#field-sidebarField')).toBeDisabled()
})
test('collection — depth field should have value 0 when empty', async () => {
const { id } = await createPost()
await page.goto(`${postsUrl.edit(id)}/api`)
const depthField = page.locator('#field-depth')
await depthField.fill('')
await expect(depthField).toHaveValue('0')
})
test('global — should not show API tab when disabled in config', async () => {
await page.goto(postsUrl.global(noApiViewGlobalSlug))
await expect(page.locator('.doc-tabs__tabs-container')).not.toContainText('API')
})
test('global — should not enable API route when disabled in config', async () => {
await page.goto(`${postsUrl.global(noApiViewGlobalSlug)}/api`)
await expect(page.locator('.not-found')).toHaveCount(1)
})
})
describe('preview button', () => {
test('collection — should render preview button when `admin.preview` is set', async () => {
const collectionWithPreview = new AdminUrlUtil(serverURL, postsCollectionSlug)
await page.goto(collectionWithPreview.create)
await page.locator('#field-title').fill(title)
await saveDocAndAssert(page)
await expect(page.locator('.btn.preview-btn')).toBeVisible()
})
test('collection — should not render preview button when `admin.preview` is not set', async () => {
const collectionWithoutPreview = new AdminUrlUtil(serverURL, group1Collection1Slug)
await page.goto(collectionWithoutPreview.create)
await page.locator('#field-title').fill(title)
await saveDocAndAssert(page)
await expect(page.locator('.btn.preview-btn')).toBeHidden()
})
test('global — should render preview button when `admin.preview` is set', async () => {
const globalWithPreview = new AdminUrlUtil(serverURL, globalSlug)
await page.goto(globalWithPreview.global(globalSlug))
await expect(page.locator('.btn.preview-btn')).toBeVisible()
})
test('global — should not render preview button when `admin.preview` is not set', async () => {
const globalWithoutPreview = new AdminUrlUtil(serverURL, group1GlobalSlug)
await page.goto(globalWithoutPreview.global(group1GlobalSlug))
await page.locator('#field-title').fill(title)
await saveDocAndAssert(page)
await expect(page.locator('.btn.preview-btn')).toBeHidden()
})
})
describe('form state', () => {
test('collection — should re-enable fields after save', async () => {
await page.goto(postsUrl.create)
await page.locator('#field-title').fill(title)
await saveDocAndAssert(page)
await expect(page.locator('#field-title')).toBeEnabled()
})
test('global — should re-enable fields after save', async () => {
await page.goto(globalURL.global(globalSlug))
await page.locator('#field-title').fill(title)
await saveDocAndAssert(page)
await expect(page.locator('#field-title')).toBeEnabled()
})
test('should thread proper event argument to validation functions', async () => {
await page.goto(postsUrl.create)
await page.locator('#field-title').fill(title)
await page.locator('#field-validateUsingEvent').fill('Not allowed')
await saveDocAndAssert(page, '#action-save', 'error')
})
})
describe('document titles', () => {
test('collection — should render fallback titles when creating new', async () => {
await page.goto(postsUrl.create)
await checkPageTitle(page, '[Untitled]')
await checkBreadcrumb(page, 'Create New')
await saveDocAndAssert(page)
expect(true).toBe(true)
})
test('collection — should render `useAsTitle` field', async () => {
await page.goto(postsUrl.create)
await page.locator('#field-title')?.fill(title)
await saveDocAndAssert(page)
await wait(500)
await checkPageTitle(page, title)
await checkBreadcrumb(page, title)
expect(true).toBe(true)
})
test('collection — should render `id` as `useAsTitle` fallback', async () => {
const { id } = await createPost()
const postURL = postsUrl.edit(id)
await page.goto(postURL)
await wait(500)
await page.locator('#field-title')?.fill('')
await expect(page.locator('.doc-header__title.render-title:has-text("ID:")')).toBeVisible()
await saveDocAndAssert(page)
})
test('global — should render custom, localized label', async () => {
await page.goto(globalURL.global(globalSlug))
await openNav(page)
const label = 'My Global Label'
const globalLabel = page.locator(`#nav-global-global`)
await expect(globalLabel).toContainText(label)
await globalLabel.click()
await checkPageTitle(page, label)
await checkBreadcrumb(page, label)
await page.locator('#field-title').fill(title)
await saveDocAndAssert(page)
await checkPageTitle(page, label)
await checkBreadcrumb(page, label)
})
test('global — should render simple label strings', async () => {
await page.goto(postsUrl.admin)
await openNav(page)
const label = 'Group Globals 1'
const globalLabel = page.locator(`#nav-global-group-globals-one`)
await expect(globalLabel).toContainText(label)
await globalLabel.click()
await checkPageTitle(page, label)
await checkBreadcrumb(page, label)
})
test('global — should render slug in sentence case as fallback', async () => {
await page.goto(postsUrl.admin)
await openNav(page)
const label = 'Group Globals Two'
const globalLabel = page.locator(`#nav-global-group-globals-two`)
await expect(globalLabel).toContainText(label)
await globalLabel.click()
await checkPageTitle(page, label)
await checkBreadcrumb(page, label)
})
})
describe('breadcrumbs', () => {
test('List drawer should not effect underlying breadcrumbs', async () => {
await navigateToDoc(page, postsUrl)
await expect(page.locator('.step-nav.app-header__step-nav a').nth(1)).toHaveText('Posts')
await page.locator('#field-upload button.upload__listToggler').click()
await expect(page.locator('[id^=list-drawer_1_]')).toBeVisible()
await wait(100) // wait for the component to re-render
await expect(
page.locator('.step-nav.app-header__step-nav .step-nav__last'),
).not.toContainText('Uploads')
await expect(page.locator('.step-nav.app-header__step-nav a').nth(1)).toHaveText('Posts')
})
})
describe('custom document views', () => {
test('collection — should render custom tab view', async () => {
await page.goto(customViewsURL.create)
await page.locator('#field-title').fill('Test')
await saveDocAndAssert(page)
const pageURL = page.url()
const customViewURL = `${pageURL}${customTabViewPath}`
await page.goto(customViewURL)
expect(page.url()).toEqual(customViewURL)
await expect(page.locator('h1#custom-view-title')).toContainText(customTabViewTitle)
})
test('collection — should render custom nested tab view', async () => {
await page.goto(customViewsURL.create)
await page.locator('#field-title').fill('Test')
await saveDocAndAssert(page)
// wait for the update view to load
await page.waitForURL(/\/(?!create$)[\w-]+$/)
const pageURL = page.url()
const customNestedTabViewURL = `${pageURL}${customNestedTabViewPath}`
await page.goto(customNestedTabViewURL)
await expect(page.locator('h1#custom-view-title')).toContainText(customNestedTabViewTitle)
})
test('collection — should render custom tab label', async () => {
await page.goto(customViewsURL.create)
await page.locator('#field-title').fill('Test')
await saveDocAndAssert(page)
// wait for the update view to load
await page.waitForURL(/\/(?!create$)[\w-]+$/)
const editTab = page.locator('.doc-tab a[tabindex="-1"]')
await expect(editTab).toContainText(customEditLabel)
})
test('collection — should render custom tab component', async () => {
await page.goto(customViewsURL.create)
await page.locator('#field-title').fill('Test')
await saveDocAndAssert(page)
const customTab = page.locator(`.doc-tab a:has-text("${customTabLabel}")`)
await expect(customTab).toBeVisible()
})
test('global — should render custom tab label', async () => {
await page.goto(globalURL.global(customGlobalViews2GlobalSlug) + '/custom-tab-view')
const title = page.locator('#custom-view-title')
const docTab = page.locator('.doc-tab__link:has-text("Custom")')
await expect(docTab).toBeVisible()
await expect(title).toContainText('Custom Tab Label View')
})
test('global — should render custom tab component', async () => {
await page.goto(globalURL.global(customGlobalViews2GlobalSlug) + '/custom-tab-component')
const title = page.locator('#custom-view-title')
const docTab = page.locator('.custom-doc-tab').first()
await expect(docTab).toBeVisible()
await expect(docTab).toContainText('Custom Tab Component')
await expect(title).toContainText('Custom View With Tab Component')
})
})
describe('drawers', () => {
test('document drawers are visually stacking', async () => {
await navigateToDoc(page, postsUrl)
await page.locator('#field-title').fill(title)
await saveDocAndAssert(page)
await page
.locator('.field-type.relationship .relationship--single-value__drawer-toggler')
.click()
await wait(500)
const drawer1Content = page.locator('[id^=doc-drawer_posts_1_] .drawer__content')
await expect(drawer1Content).toBeVisible()
const drawerLeft = await drawer1Content.boundingBox().then((box) => box.x)
await drawer1Content
.locator('.field-type.relationship .relationship--single-value__drawer-toggler')
.click()
const drawer2Content = page.locator('[id^=doc-drawer_posts_2_] .drawer__content')
await expect(drawer2Content).toBeVisible()
const drawer2Left = await drawer2Content.boundingBox().then((box) => box.x)
expect(drawer2Left > drawerLeft).toBe(true)
})
})
describe('descriptions', () => {
test('should render tab admin description', async () => {
await page.goto(postsUrl.create)
const tabsContent = page.locator('.tabs-field__content-wrap')
await expect(tabsContent.locator('.field-description')).toHaveText(customTabAdminDescription)
})
test('should render tab admin description as a translation function', async () => {
await page.goto(postsUrl.create)
const secondTab = page.locator('.tabs-field__tab-button').nth(1)
await secondTab.click()
await wait(500)
const tabsContent = page.locator('.tabs-field__content-wrap')
await expect(
tabsContent.locator('.field-description', { hasText: `t:${customTabAdminDescription}` }),
).toBeVisible()
})
})
describe('custom fields', () => {
test('should render custom field component', async () => {
await page.goto(customFieldsURL.create)
await expect(page.locator('#field-customTextClientField')).toBeVisible()
})
test('renders custom label component', async () => {
await page.goto(customFieldsURL.create)
await expect(page.locator('#custom-client-field-label')).toBeVisible()
await expect(page.locator('#custom-server-field-label')).toBeVisible()
})
test('renders custom field description text', async () => {
await page.goto(customFieldsURL.create)
await expect(page.locator('#custom-client-field-description')).toBeVisible()
await expect(page.locator('#custom-server-field-description')).toBeVisible()
})
test('custom server components should receive field props', async () => {
await page.goto(customFieldsURL.create)
await expect(
page.locator('#custom-server-field-label', {
hasText: exactText('Label: the max length of this field is: 100'),
}),
).toBeVisible()
await expect(
page.locator('#custom-server-field-description', {
hasText: exactText('Description: the max length of this field is: 100'),
}),
).toBeVisible()
})
test('custom client components should receive field props', async () => {
await page.goto(customFieldsURL.create)
await expect(
page.locator('#custom-client-field-label', {
hasText: exactText('Label: the max length of this field is: 100'),
}),
).toBeVisible()
await expect(
page.locator('#custom-client-field-description', {
hasText: exactText('Description: the max length of this field is: 100'),
}),
).toBeVisible()
})
test('custom select input can have its value cleared', async () => {
await page.goto(customFieldsURL.create)
await expect(page.locator('#field-customSelectInput')).toBeVisible()
await page.locator('#field-customSelectInput .rs__control').click()
await page.locator('#field-customSelectInput .rs__option').first().click()
await expect(page.locator('#field-customSelectInput .rs__single-value')).toHaveText(
'Option 1',
)
await page.locator('.clear-value').click()
await expect(page.locator('#field-customSelectInput .rs__placeholder')).toHaveText(
'Select a value',
)
})
describe('field descriptions', () => {
test('should render static field description', async () => {
await page.goto(customFieldsURL.create)
await expect(page.locator('.field-description-descriptionAsString')).toContainText(
'Static field description.',
)
})
test('should render functional field description', async () => {
await page.goto(customFieldsURL.create)
await page.locator('#field-descriptionAsFunction').fill('functional')
await expect(page.locator('.field-description-descriptionAsFunction')).toContainText(
'Function description',
)
})
})
test('should render component field description', async () => {
await page.goto(customFieldsURL.create)
await page.locator('#field-descriptionAsComponent').fill('component')
await expect(page.locator('.field-description-descriptionAsComponent')).toContainText(
'Component description: descriptionAsComponent - component',
)
})
test('should render custom error component', async () => {
await page.goto(customFieldsURL.create)
const input = page.locator('input[id="field-customTextClientField"]')
await input.fill('ab')
await expect(input).toHaveValue('ab')
const error = page.locator('.custom-error:near(input[id="field-customTextClientField"])')
const submit = page.locator('button[type="button"][id="action-save"]')
await submit.click()
await expect(error).toHaveText('#custom-error')
})
test('should render beforeInput and afterInput', async () => {
await page.goto(customFieldsURL.create)
const input = page.locator('input[id="field-customTextClientField"]')
const prevSibling = await input.evaluateHandle((el) => {
return el.previousElementSibling
})
const prevSiblingText = await page.evaluate((el) => el?.textContent, prevSibling)
expect(prevSiblingText).toEqual('#before-input')
const nextSibling = await input.evaluateHandle((el) => {
return el.nextElementSibling
})
const nextSiblingText = await page.evaluate((el) => el?.textContent, nextSibling)
expect(nextSiblingText).toEqual('#after-input')
})
describe('select field', () => {
test('should render custom select options', async () => {
await page.goto(customFieldsURL.create)
await page.locator('#field-customSelectField .rs__control').click()
await expect(page.locator('#field-customSelectField .rs__option')).toHaveCount(2)
})
})
})
describe('publish button', () => {
test('should show publish active locale button with defaultLocalePublishOption', async () => {
await navigateToDoc(page, postsUrl)
const publishButton = page.locator('#action-save')
await expect(publishButton).toBeVisible()
await expect(publishButton).toContainText('Publish in English')
})
})
})
async function createPost(overrides?: Partial<Post>): Promise<Post> {
return payload.create({
collection: postsCollectionSlug,
data: {
description,
title,
...overrides,
},
}) as unknown as Promise<Post>
}