Merge branch 'main' into fix/parent-labels-in-toast
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
openNav,
|
||||
saveDocAndAssert,
|
||||
saveDocHotkeyAndAssert,
|
||||
// throttleTest,
|
||||
} from '../../../helpers.js'
|
||||
import { AdminUrlUtil } from '../../../helpers/adminUrlUtil.js'
|
||||
import { initPayloadE2ENoConfig } from '../../../helpers/initPayloadE2ENoConfig.js'
|
||||
@@ -100,6 +101,12 @@ describe('General', () => {
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
// await throttleTest({
|
||||
// page,
|
||||
// context,
|
||||
// delay: 'Fast 4G',
|
||||
// })
|
||||
|
||||
await reInitializeDB({
|
||||
serverURL,
|
||||
snapshotKey: 'adminTests',
|
||||
@@ -721,25 +728,32 @@ describe('General', () => {
|
||||
'Deleted 3 Posts successfully.',
|
||||
)
|
||||
|
||||
await expect(page.locator('.collection-list__no-results')).toBeVisible()
|
||||
// Poll until router has refreshed
|
||||
await expect.poll(() => page.locator('.collection-list__no-results').isVisible()).toBeTruthy()
|
||||
})
|
||||
|
||||
test('should bulk delete with filters and across pages', async () => {
|
||||
await deleteAllPosts()
|
||||
await Promise.all([createPost({ title: 'Post 1' }), createPost({ title: 'Post 2' })])
|
||||
|
||||
Array.from({ length: 6 }).forEach(async (_, i) => {
|
||||
await createPost({ title: `Post ${i + 1}` })
|
||||
})
|
||||
|
||||
await page.goto(postsUrl.list)
|
||||
await page.locator('#search-filter-input').fill('Post 1')
|
||||
await expect(page.locator('.table table > tbody > tr')).toHaveCount(1)
|
||||
await page.locator('#search-filter-input').fill('Post')
|
||||
await page.waitForURL(/search=Post/)
|
||||
await expect(page.locator('.table table > tbody > tr')).toHaveCount(5)
|
||||
await page.locator('input#select-all').check()
|
||||
await page.locator('button.list-selection__button').click()
|
||||
await page.locator('button#select-all-across-pages').click()
|
||||
await page.locator('.delete-documents__toggle').click()
|
||||
await page.locator('#delete-posts #confirm-action').click()
|
||||
|
||||
await expect(page.locator('.payload-toast-container .toast-success')).toHaveText(
|
||||
'Deleted 1 Post successfully.',
|
||||
'Deleted 6 Posts successfully.',
|
||||
)
|
||||
|
||||
await expect(page.locator('.table table > tbody > tr')).toHaveCount(1)
|
||||
// Poll until router has refreshed
|
||||
await expect.poll(() => page.locator('.table table > tbody > tr').count()).toBe(0)
|
||||
})
|
||||
|
||||
test('should bulk update', async () => {
|
||||
@@ -835,17 +849,30 @@ describe('General', () => {
|
||||
expect(updatedPost.docs[0].defaultValueField).toBe('not the default value')
|
||||
})
|
||||
|
||||
test('should not show "select all across pages" button if already selected all', async () => {
|
||||
await deleteAllPosts()
|
||||
await createPost({ title: `Post 1` })
|
||||
await page.goto(postsUrl.list)
|
||||
await page.locator('input#select-all').check()
|
||||
await expect(page.locator('button#select-all-across-pages')).toBeHidden()
|
||||
})
|
||||
|
||||
test('should bulk update with filters and across pages', async () => {
|
||||
// First, delete all posts created by the seed
|
||||
await deleteAllPosts()
|
||||
const post1Title = 'Post 1'
|
||||
await Promise.all([createPost({ title: post1Title }), createPost({ title: 'Post 2' })])
|
||||
const updatedPostTitle = `${post1Title} (Updated)`
|
||||
|
||||
Array.from({ length: 6 }).forEach(async (_, i) => {
|
||||
await createPost({ title: `Post ${i + 1}` })
|
||||
})
|
||||
|
||||
await page.goto(postsUrl.list)
|
||||
await page.locator('#search-filter-input').fill('Post 1')
|
||||
await expect(page.locator('.table table > tbody > tr')).toHaveCount(1)
|
||||
await page.locator('#search-filter-input').fill('Post')
|
||||
await page.waitForURL(/search=Post/)
|
||||
await expect(page.locator('.table table > tbody > tr')).toHaveCount(5)
|
||||
|
||||
await page.locator('input#select-all').check()
|
||||
await page.locator('button.list-selection__button').click()
|
||||
await page.locator('button#select-all-across-pages').click()
|
||||
|
||||
await page.locator('.edit-many__toggle').click()
|
||||
await page.locator('.field-select .rs__control').click()
|
||||
|
||||
@@ -857,23 +884,29 @@ describe('General', () => {
|
||||
await titleOption.click()
|
||||
const titleInput = page.locator('#field-title')
|
||||
await expect(titleInput).toBeVisible()
|
||||
await titleInput.fill(updatedPostTitle)
|
||||
const updatedTitle = `Post (Updated)`
|
||||
await titleInput.fill(updatedTitle)
|
||||
|
||||
await page.locator('.form-submit button[type="submit"].edit-many__publish').click()
|
||||
await expect(page.locator('.payload-toast-container .toast-success')).toContainText(
|
||||
'Updated 1 Post successfully.',
|
||||
'Updated 6 Posts successfully.',
|
||||
)
|
||||
|
||||
await expect(page.locator('.table table > tbody > tr')).toHaveCount(1)
|
||||
await expect(page.locator('.row-1 .cell-title')).toContainText(updatedPostTitle)
|
||||
// Poll until router has refreshed
|
||||
await expect.poll(() => page.locator('.table table > tbody > tr').count()).toBe(5)
|
||||
await expect(page.locator('.row-1 .cell-title')).toContainText(updatedTitle)
|
||||
})
|
||||
|
||||
test('should update selection state after deselecting item following select all', async () => {
|
||||
await deleteAllPosts()
|
||||
await createPost({ title: 'Post 1' })
|
||||
|
||||
Array.from({ length: 6 }).forEach(async (_, i) => {
|
||||
await createPost({ title: `Post ${i + 1}` })
|
||||
})
|
||||
|
||||
await page.goto(postsUrl.list)
|
||||
await page.locator('input#select-all').check()
|
||||
await page.locator('button.list-selection__button').click()
|
||||
await page.locator('button#select-all-across-pages').click()
|
||||
|
||||
// Deselect the first row
|
||||
await page.locator('.row-1 input').click()
|
||||
|
||||
8
test/config/bin.ts
Normal file
8
test/config/bin.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
const { bin } = await import(path.resolve(dirname, '../../packages/payload/src/bin/index.js'))
|
||||
|
||||
await bin()
|
||||
@@ -80,6 +80,12 @@ export default buildConfigWithDefaults({
|
||||
path: '/config',
|
||||
},
|
||||
],
|
||||
bin: [
|
||||
{
|
||||
scriptPath: path.resolve(dirname, 'customScript.ts'),
|
||||
key: 'start-server',
|
||||
},
|
||||
],
|
||||
globals: [
|
||||
{
|
||||
slug: 'my-global',
|
||||
@@ -107,13 +113,17 @@ export default buildConfigWithDefaults({
|
||||
},
|
||||
],
|
||||
onInit: async (payload) => {
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: devUser.email,
|
||||
password: devUser.password,
|
||||
},
|
||||
})
|
||||
const { totalDocs } = await payload.count({ collection: 'users' })
|
||||
|
||||
if (totalDocs === 0) {
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: devUser.email,
|
||||
password: devUser.password,
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
typescript: {
|
||||
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
||||
|
||||
13
test/config/customScript.ts
Normal file
13
test/config/customScript.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { SanitizedConfig } from 'payload'
|
||||
|
||||
import { writeFileSync } from 'fs'
|
||||
import payload from 'payload'
|
||||
|
||||
import { testFilePath } from './testFilePath.js'
|
||||
|
||||
export const script = async (config: SanitizedConfig) => {
|
||||
await payload.init({ config })
|
||||
const data = await payload.find({ collection: 'users' })
|
||||
writeFileSync(testFilePath, JSON.stringify(data), 'utf-8')
|
||||
process.exit(0)
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
import type { BlockField, Payload } from 'payload'
|
||||
|
||||
import { execSync } from 'child_process'
|
||||
import { existsSync, readFileSync, rmSync } from 'fs'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import type { NextRESTClient } from '../helpers/NextRESTClient.js'
|
||||
|
||||
import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
||||
import { testFilePath } from './testFilePath.js'
|
||||
|
||||
let restClient: NextRESTClient
|
||||
let payload: Payload
|
||||
@@ -106,4 +109,31 @@ describe('Config', () => {
|
||||
expect(response.headers.get('Access-Control-Allow-Headers')).toContain('x-custom-header')
|
||||
})
|
||||
})
|
||||
|
||||
describe('bin config', () => {
|
||||
const executeCLI = (command: string) => {
|
||||
execSync(`pnpm tsx "${path.resolve(dirname, 'bin.ts')}" ${command}`, {
|
||||
env: {
|
||||
...process.env,
|
||||
PAYLOAD_CONFIG_PATH: path.resolve(dirname, 'config.ts'),
|
||||
PAYLOAD_DROP_DATABASE: 'false',
|
||||
},
|
||||
stdio: 'inherit',
|
||||
cwd: path.resolve(dirname, '../..'), // from root
|
||||
})
|
||||
}
|
||||
|
||||
const deleteTestFile = () => {
|
||||
if (existsSync(testFilePath)) {
|
||||
rmSync(testFilePath)
|
||||
}
|
||||
}
|
||||
|
||||
it('should execute a custom script', () => {
|
||||
deleteTestFile()
|
||||
executeCLI('start-server')
|
||||
expect(JSON.parse(readFileSync(testFilePath, 'utf-8')).docs).toHaveLength(1)
|
||||
deleteTestFile()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
7
test/config/testFilePath.ts
Normal file
7
test/config/testFilePath.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
export const testFilePath = path.resolve(dirname, '_data.json')
|
||||
@@ -88,6 +88,28 @@ export const Relationship: CollectionConfig = {
|
||||
relationTo: slug,
|
||||
type: 'relationship',
|
||||
},
|
||||
{
|
||||
type: 'collapsible',
|
||||
label: 'Collapsible',
|
||||
fields: [
|
||||
{
|
||||
name: 'nestedRelationshipFilteredByField',
|
||||
filterOptions: () => {
|
||||
return {
|
||||
filter: {
|
||||
equals: 'Include me',
|
||||
},
|
||||
}
|
||||
},
|
||||
admin: {
|
||||
description:
|
||||
'This will filter the relationship options if the filter field in this document is set to "Include me"',
|
||||
},
|
||||
relationTo: slug,
|
||||
type: 'relationship',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'relationshipFilteredAsync',
|
||||
filterOptions: (args: FilterOptionsProps<FieldsRelationship>) => {
|
||||
|
||||
@@ -351,6 +351,41 @@ describe('Relationship Field', () => {
|
||||
await expect(valueOptions.locator(`text=${idToInclude}`)).toBeVisible()
|
||||
})
|
||||
|
||||
test('should apply filter options of nested fields to list view filter controls', async () => {
|
||||
const { id: idToInclude } = await payload.create({
|
||||
collection: slug,
|
||||
data: {
|
||||
filter: 'Include me',
|
||||
},
|
||||
})
|
||||
|
||||
// first ensure that filter options are applied in the edit view
|
||||
await page.goto(url.edit(idToInclude))
|
||||
const field = page.locator('#field-nestedRelationshipFilteredByField')
|
||||
await field.click({ delay: 100 })
|
||||
const options = field.locator('.rs__option')
|
||||
await expect(options).toHaveCount(1)
|
||||
await expect(options).toContainText(idToInclude)
|
||||
|
||||
// now ensure that the same filter options are applied in the list view
|
||||
await page.goto(url.list)
|
||||
|
||||
const whereBuilder = await addListFilter({
|
||||
page,
|
||||
fieldLabel: 'Collapsible > Nested Relationship Filtered By Field',
|
||||
operatorLabel: 'equals',
|
||||
skipValueInput: true,
|
||||
})
|
||||
|
||||
const valueInput = page.locator('.condition__value input')
|
||||
await valueInput.click()
|
||||
const valueOptions = whereBuilder.locator('.condition__value .rs__option')
|
||||
|
||||
await expect(valueOptions).toHaveCount(2)
|
||||
await expect(valueOptions.locator(`text=None`)).toBeVisible()
|
||||
await expect(valueOptions.locator(`text=${idToInclude}`)).toBeVisible()
|
||||
})
|
||||
|
||||
test('should allow usage of relationTo in filterOptions', async () => {
|
||||
const { id: include } = (await payload.create({
|
||||
collection: relationOneSlug,
|
||||
|
||||
@@ -177,6 +177,10 @@ export interface FieldsRelationship {
|
||||
* This will filter the relationship options if the filter field in this document is set to "Include me"
|
||||
*/
|
||||
relationshipFilteredByField?: (string | null) | FieldsRelationship;
|
||||
/**
|
||||
* This will filter the relationship options if the filter field in this document is set to "Include me"
|
||||
*/
|
||||
nestedRelationshipFilteredByField?: (string | null) | FieldsRelationship;
|
||||
relationshipFilteredAsync?: (string | null) | RelationOne;
|
||||
relationshipManyFiltered?:
|
||||
| (
|
||||
@@ -506,6 +510,7 @@ export interface FieldsRelationshipSelect<T extends boolean = true> {
|
||||
relationshipWithTitle?: T;
|
||||
relationshipFilteredByID?: T;
|
||||
relationshipFilteredByField?: T;
|
||||
nestedRelationshipFilteredByField?: T;
|
||||
relationshipFilteredAsync?: T;
|
||||
relationshipManyFiltered?: T;
|
||||
filter?: T;
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { BrowserContext, Page } from '@playwright/test'
|
||||
import { expect, test } from '@playwright/test'
|
||||
import { addBlock } from 'helpers/e2e/addBlock.js'
|
||||
import { openBlocksDrawer } from 'helpers/e2e/openBlocksDrawer.js'
|
||||
import { reorderBlocks } from 'helpers/e2e/reorderBlocks.js'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
@@ -289,6 +290,39 @@ describe('Block fields', () => {
|
||||
})
|
||||
|
||||
describe('row manipulation', () => {
|
||||
test('moving rows should immediately move custom row labels', async () => {
|
||||
await page.goto(url.create)
|
||||
|
||||
// first ensure that the first block has the custom header, and that the second block doesn't
|
||||
|
||||
await expect(
|
||||
page.locator('#field-blocks #blocks-row-0 .blocks-field__block-header'),
|
||||
).toHaveText('Custom Block Label: Content 01')
|
||||
|
||||
const secondBlockHeader = page.locator(
|
||||
'#field-blocks #blocks-row-1 .blocks-field__block-header',
|
||||
)
|
||||
|
||||
await expect(secondBlockHeader.locator('.blocks-field__block-pill')).toHaveText('Number')
|
||||
|
||||
await expect(secondBlockHeader.locator('input[id="blocks.1.blockName"]')).toHaveValue(
|
||||
'Second block',
|
||||
)
|
||||
|
||||
await reorderBlocks({
|
||||
page,
|
||||
fieldName: 'blocks',
|
||||
fromBlockIndex: 0,
|
||||
toBlockIndex: 1,
|
||||
})
|
||||
|
||||
// Important: do _not_ poll here, use `textContent()` instead of `toHaveText()`
|
||||
// This will prevent Playwright from polling for the change to the DOM
|
||||
expect(
|
||||
await page.locator('#field-blocks #blocks-row-1 .blocks-field__block-header').textContent(),
|
||||
).toMatch(/^Custom Block Label: Content/)
|
||||
})
|
||||
|
||||
describe('react hooks', () => {
|
||||
test('should add 2 new block rows', async () => {
|
||||
await page.goto(url.create)
|
||||
|
||||
38
test/fields/collections/JSON/AfterField.tsx
Normal file
38
test/fields/collections/JSON/AfterField.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
'use client'
|
||||
|
||||
import { useField } from '@payloadcms/ui'
|
||||
|
||||
export function AfterField() {
|
||||
const { setValue } = useField({ path: 'customJSON' })
|
||||
|
||||
return (
|
||||
<button
|
||||
id="set-custom-json"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
setValue({
|
||||
users: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'John Doe',
|
||||
email: 'john.doe@example.com',
|
||||
isActive: true,
|
||||
roles: ['admin', 'editor'],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Jane Smith',
|
||||
email: 'jane.smith@example.com',
|
||||
isActive: false,
|
||||
roles: ['viewer'],
|
||||
},
|
||||
],
|
||||
})
|
||||
}}
|
||||
style={{ marginTop: '5px', padding: '5px 10px' }}
|
||||
type="button"
|
||||
>
|
||||
Set Custom JSON
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -103,4 +103,24 @@ describe('JSON', () => {
|
||||
'"foo.with.periods": "bar"',
|
||||
)
|
||||
})
|
||||
|
||||
test('should update', async () => {
|
||||
const createdDoc = await payload.create({
|
||||
collection: 'json-fields',
|
||||
data: {
|
||||
customJSON: {
|
||||
default: 'value',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
await page.goto(url.edit(createdDoc.id))
|
||||
const jsonField = page.locator('.json-field #field-customJSON')
|
||||
await expect(jsonField).toContainText('"default": "value"')
|
||||
|
||||
const originalHeight = (await page.locator('#field-customJSON').boundingBox())?.height || 0
|
||||
await page.locator('#set-custom-json').click()
|
||||
const newHeight = (await page.locator('#field-customJSON').boundingBox())?.height || 0
|
||||
expect(newHeight).toBeGreaterThan(originalHeight)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -67,6 +67,16 @@ const JSON: CollectionConfig = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'customJSON',
|
||||
type: 'json',
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: ['./collections/JSON/AfterField#AfterField'],
|
||||
},
|
||||
},
|
||||
label: 'Custom Json',
|
||||
},
|
||||
],
|
||||
versions: {
|
||||
maxPerDoc: 1,
|
||||
|
||||
@@ -170,12 +170,7 @@ describe('Text', () => {
|
||||
user: client.user,
|
||||
key: 'text-fields-list',
|
||||
value: {
|
||||
columns: [
|
||||
{
|
||||
accessor: 'disableListColumnText',
|
||||
active: true,
|
||||
},
|
||||
],
|
||||
columns: [{ disableListColumnText: true }],
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -165,6 +165,68 @@ describe('Fields', () => {
|
||||
expect(missResult).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should query like on value', async () => {
|
||||
const miss = await payload.create({
|
||||
collection: 'text-fields',
|
||||
data: {
|
||||
text: 'dog',
|
||||
},
|
||||
})
|
||||
|
||||
const hit = await payload.create({
|
||||
collection: 'text-fields',
|
||||
data: {
|
||||
text: 'cat',
|
||||
},
|
||||
})
|
||||
|
||||
const { docs } = await payload.find({
|
||||
collection: 'text-fields',
|
||||
where: {
|
||||
text: {
|
||||
like: 'cat',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const hitResult = docs.find(({ id: findID }) => hit.id === findID)
|
||||
const missResult = docs.find(({ id: findID }) => miss.id === findID)
|
||||
|
||||
expect(hitResult).toBeDefined()
|
||||
expect(missResult).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should query not_like on value', async () => {
|
||||
const hit = await payload.create({
|
||||
collection: 'text-fields',
|
||||
data: {
|
||||
text: 'dog',
|
||||
},
|
||||
})
|
||||
|
||||
const miss = await payload.create({
|
||||
collection: 'text-fields',
|
||||
data: {
|
||||
text: 'cat',
|
||||
},
|
||||
})
|
||||
|
||||
const { docs } = await payload.find({
|
||||
collection: 'text-fields',
|
||||
where: {
|
||||
text: {
|
||||
not_like: 'cat',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const hitResult = docs.find(({ id: findID }) => hit.id === findID)
|
||||
const missResult = docs.find(({ id: findID }) => miss.id === findID)
|
||||
|
||||
expect(hitResult).toBeDefined()
|
||||
expect(missResult).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should query hasMany within an array', async () => {
|
||||
const docFirst = await payload.create({
|
||||
collection: 'text-fields',
|
||||
@@ -2713,6 +2775,20 @@ describe('Fields', () => {
|
||||
expect(docIDs).not.toContain(bazBar.id)
|
||||
})
|
||||
|
||||
it('should query nested properties - not_like', async () => {
|
||||
const { docs } = await payload.find({
|
||||
collection: 'json-fields',
|
||||
where: {
|
||||
'json.baz': { not_like: 'bar' },
|
||||
},
|
||||
})
|
||||
|
||||
const docIDs = docs.map(({ id }) => id)
|
||||
|
||||
expect(docIDs).toContain(fooBar.id)
|
||||
expect(docIDs).not.toContain(bazBar.id)
|
||||
})
|
||||
|
||||
it('should query nested properties - equals', async () => {
|
||||
const { docs } = await payload.find({
|
||||
collection: 'json-fields',
|
||||
|
||||
@@ -1474,6 +1474,15 @@ export interface JsonField {
|
||||
| boolean
|
||||
| null;
|
||||
};
|
||||
customJSON?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
@@ -3165,6 +3174,7 @@ export interface JsonFieldsSelect<T extends boolean = true> {
|
||||
| {
|
||||
jsonWithinGroup?: T;
|
||||
};
|
||||
customJSON?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
|
||||
@@ -391,9 +391,13 @@ export async function switchTab(page: Page, selector: string) {
|
||||
*
|
||||
* Useful to prevent the e2e test from passing when, for example, there are react missing key prop errors
|
||||
* @param page
|
||||
* @param options
|
||||
*/
|
||||
export function initPageConsoleErrorCatch(page: Page, options?: { ignoreCORS?: boolean }) {
|
||||
const { ignoreCORS = false } = options || {} // Default to not ignoring CORS errors
|
||||
const consoleErrors: string[] = []
|
||||
|
||||
let shouldCollectErrors = false
|
||||
|
||||
page.on('console', (msg) => {
|
||||
if (
|
||||
@@ -435,6 +439,21 @@ export function initPageConsoleErrorCatch(page: Page, options?: { ignoreCORS?: b
|
||||
console.log(`Ignoring expected network error: ${msg.text()}`)
|
||||
}
|
||||
})
|
||||
|
||||
// Capture uncaught errors that do not appear in the console
|
||||
page.on('pageerror', (error) => {
|
||||
if (shouldCollectErrors) {
|
||||
consoleErrors.push(`Page error: ${error.message}`)
|
||||
} else {
|
||||
throw new Error(`Page error: ${error.message}`)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
consoleErrors,
|
||||
collectErrors: () => (shouldCollectErrors = true), // Enable collection of errors for specific tests
|
||||
stopCollectingErrors: () => (shouldCollectErrors = false), // Disable collection of errors after the test
|
||||
}
|
||||
}
|
||||
|
||||
export function describeIfInCIOrHasLocalstack(): jest.Describe {
|
||||
|
||||
@@ -175,7 +175,10 @@ describe('Joins Field', () => {
|
||||
collection: categoriesSlug,
|
||||
})
|
||||
|
||||
expect(Object.keys(categoryWithPosts)).toStrictEqual(['id', 'group'])
|
||||
expect(categoryWithPosts).toStrictEqual({
|
||||
id: categoryWithPosts.id,
|
||||
group: categoryWithPosts.group,
|
||||
})
|
||||
|
||||
expect(categoryWithPosts.group.relatedPosts.docs).toHaveLength(10)
|
||||
expect(categoryWithPosts.group.relatedPosts.docs[0]).toHaveProperty('id')
|
||||
@@ -1202,6 +1205,37 @@ describe('Joins Field', () => {
|
||||
expect(parent.children.docs[1]?.value.id).toBe(child_1.id)
|
||||
expect(parent.children.docs[1]?.relationTo).toBe('multiple-collections-1')
|
||||
|
||||
// Pagination across collections
|
||||
parent = await payload.findByID({
|
||||
collection: 'multiple-collections-parents',
|
||||
id: parent.id,
|
||||
depth: 1,
|
||||
joins: {
|
||||
children: {
|
||||
limit: 1,
|
||||
sort: 'title',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(parent.children.docs).toHaveLength(1)
|
||||
expect(parent.children?.hasNextPage).toBe(true)
|
||||
|
||||
parent = await payload.findByID({
|
||||
collection: 'multiple-collections-parents',
|
||||
id: parent.id,
|
||||
depth: 1,
|
||||
joins: {
|
||||
children: {
|
||||
limit: 2,
|
||||
sort: 'title',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(parent.children.docs).toHaveLength(2)
|
||||
expect(parent.children?.hasNextPage).toBe(false)
|
||||
|
||||
// Sorting across collections
|
||||
parent = await payload.findByID({
|
||||
collection: 'multiple-collections-parents',
|
||||
|
||||
@@ -64,8 +64,16 @@ export default buildConfigWithDefaults({
|
||||
NestedArray,
|
||||
NestedFields,
|
||||
{
|
||||
admin: {
|
||||
listSearchableFields: 'name',
|
||||
},
|
||||
auth: true,
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
label: { en: 'Full name' },
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'relation',
|
||||
relationTo: localizedPostsSlug,
|
||||
@@ -83,6 +91,7 @@ export default buildConfigWithDefaults({
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
label: { en: 'Full title' },
|
||||
index: true,
|
||||
localized: true,
|
||||
type: 'text',
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import type { BrowserContext, Page } from '@playwright/test'
|
||||
import type { GeneratedTypes } from 'helpers/sdk/types.js'
|
||||
|
||||
import { expect, test } from '@playwright/test'
|
||||
import { navigateToDoc } from 'helpers/e2e/navigateToDoc.js'
|
||||
import { openDocControls } from 'helpers/e2e/openDocControls.js'
|
||||
import { upsertPrefs } from 'helpers/e2e/upsertPrefs.js'
|
||||
import { RESTClient } from 'helpers/rest.js'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
@@ -31,11 +35,6 @@ import {
|
||||
spanishLocale,
|
||||
withRequiredLocalizedFields,
|
||||
} from './shared.js'
|
||||
import { navigateToDoc } from 'helpers/e2e/navigateToDoc.js'
|
||||
|
||||
import { upsertPrefs } from 'helpers/e2e/upsertPrefs.js'
|
||||
import { RESTClient } from 'helpers/rest.js'
|
||||
import { GeneratedTypes } from 'helpers/sdk/types.js'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
@@ -119,16 +118,16 @@ describe('Localization', () => {
|
||||
|
||||
await expect(page.locator('.localizer .popup')).toHaveClass(/popup--active/)
|
||||
|
||||
const activeOption = await page.locator(
|
||||
const activeOption = page.locator(
|
||||
`.localizer .popup.popup--active .popup-button-list__button--selected`,
|
||||
)
|
||||
|
||||
await expect(activeOption).toBeVisible()
|
||||
const tagName = await activeOption.evaluate((node) => node.tagName)
|
||||
await expect(tagName).not.toBe('A')
|
||||
expect(tagName).not.toBe('A')
|
||||
await expect(activeOption).not.toHaveAttribute('href')
|
||||
await expect(tagName).not.toBe('BUTTON')
|
||||
await expect(tagName).toBe('DIV')
|
||||
expect(tagName).not.toBe('BUTTON')
|
||||
expect(tagName).toBe('DIV')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -140,7 +139,7 @@ describe('Localization', () => {
|
||||
const createNewButtonLocator =
|
||||
'.collection-list a[href="/admin/collections/cannot-create-default-locale/create"]'
|
||||
|
||||
await expect(page.locator(createNewButtonLocator)).not.toBeVisible()
|
||||
await expect(page.locator(createNewButtonLocator)).toBeHidden()
|
||||
await changeLocale(page, spanishLocale)
|
||||
await expect(page.locator(createNewButtonLocator).first()).toBeVisible()
|
||||
await page.goto(urlCannotCreateDefaultLocale.create)
|
||||
@@ -330,11 +329,11 @@ describe('Localization', () => {
|
||||
|
||||
await page.goto(url.list)
|
||||
|
||||
const localeLabel = await page
|
||||
const localeLabel = page
|
||||
.locator('.localizer.app-header__localizer .localizer-button__current-label')
|
||||
.innerText()
|
||||
|
||||
|
||||
expect(localeLabel).not.toEqual('English')
|
||||
await expect(localeLabel).not.toHaveText('English')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -351,7 +350,7 @@ describe('Localization', () => {
|
||||
await navigateToDoc(page, urlRelationshipLocalized)
|
||||
const drawerToggler =
|
||||
'#field-relationMultiRelationTo .relationship--single-value__drawer-toggler'
|
||||
expect(page.locator(drawerToggler)).toBeEnabled()
|
||||
await expect(page.locator(drawerToggler)).toBeEnabled()
|
||||
await openDocDrawer(page, drawerToggler)
|
||||
await expect(page.locator('.doc-drawer__header-text')).toContainText('spanish-relation2')
|
||||
await page.locator('.doc-drawer__header-close').click()
|
||||
@@ -518,7 +517,7 @@ describe('Localization', () => {
|
||||
|
||||
// only throttle test after initial load to avoid timeouts
|
||||
const cdpSession = await throttleTest({
|
||||
page: page,
|
||||
page,
|
||||
context,
|
||||
delay: 'Fast 4G',
|
||||
})
|
||||
@@ -541,6 +540,13 @@ describe('Localization', () => {
|
||||
await cdpSession.detach()
|
||||
})
|
||||
})
|
||||
|
||||
test('should use label in search filter when string or object', async () => {
|
||||
await page.goto(url.list)
|
||||
const searchInput = page.locator('.search-filter__input')
|
||||
await expect(searchInput).toBeVisible()
|
||||
await expect(searchInput).toHaveAttribute('placeholder', 'Search by Full title')
|
||||
})
|
||||
})
|
||||
|
||||
async function fillValues(data: Partial<LocalizedPost>) {
|
||||
|
||||
@@ -64,7 +64,6 @@ export interface Config {
|
||||
auth: {
|
||||
users: UserAuthOperations;
|
||||
};
|
||||
blocks: {};
|
||||
collections: {
|
||||
richText: RichText;
|
||||
'blocks-fields': BlocksField;
|
||||
@@ -322,6 +321,7 @@ export interface NestedFieldTable {
|
||||
*/
|
||||
export interface User {
|
||||
id: string;
|
||||
name?: string | null;
|
||||
relation?: (string | null) | LocalizedPost;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
@@ -928,6 +928,7 @@ export interface NestedFieldTablesSelect<T extends boolean = true> {
|
||||
* via the `definition` "users_select".
|
||||
*/
|
||||
export interface UsersSelect<T extends boolean = true> {
|
||||
name?: T;
|
||||
relation?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { NextRESTClient } from '../helpers/NextRESTClient.js'
|
||||
|
||||
import { devUser } from '../credentials.js'
|
||||
import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
||||
import { tenantsSlug } from './shared.js'
|
||||
|
||||
let payload: Payload
|
||||
let restClient: NextRESTClient
|
||||
@@ -40,7 +41,7 @@ describe('@payloadcms/plugin-multi-tenant', () => {
|
||||
describe('tenants', () => {
|
||||
it('should create a tenant', async () => {
|
||||
const tenant1 = await payload.create({
|
||||
collection: 'tenants',
|
||||
collection: tenantsSlug,
|
||||
data: {
|
||||
name: 'tenant1',
|
||||
domain: 'tenant1.com',
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import type { Config } from 'payload'
|
||||
|
||||
import { devUser } from '../../credentials.js'
|
||||
import { menuItemsSlug, menuSlug, usersSlug } from '../shared.js'
|
||||
import { menuItemsSlug, menuSlug, tenantsSlug, usersSlug } from '../shared.js'
|
||||
|
||||
export const seed: Config['onInit'] = async (payload) => {
|
||||
// create tenants
|
||||
const blueDogTenant = await payload.create({
|
||||
collection: 'tenants',
|
||||
collection: tenantsSlug,
|
||||
data: {
|
||||
name: 'Blue Dog',
|
||||
domain: 'bluedog.com',
|
||||
},
|
||||
})
|
||||
const steelCatTenant = await payload.create({
|
||||
collection: 'tenants',
|
||||
collection: tenantsSlug,
|
||||
data: {
|
||||
name: 'Steel Cat',
|
||||
domain: 'steelcat.com',
|
||||
|
||||
@@ -76,7 +76,6 @@ describe('@payloadcms/plugin-nested-docs', () => {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// update parent doc
|
||||
await payload.update({
|
||||
collection: 'pages',
|
||||
@@ -110,6 +109,91 @@ describe('@payloadcms/plugin-nested-docs', () => {
|
||||
// @ts-ignore
|
||||
expect(lastUpdatedChildBreadcrumbs[0].url).toStrictEqual('/11-children-updated')
|
||||
})
|
||||
|
||||
it('should return breadcrumbs as an array of objects', async () => {
|
||||
const parentDoc = await payload.create({
|
||||
collection: 'pages',
|
||||
data: {
|
||||
title: 'parent doc',
|
||||
slug: 'parent-doc',
|
||||
_status: 'published',
|
||||
},
|
||||
})
|
||||
|
||||
const childDoc = await payload.create({
|
||||
collection: 'pages',
|
||||
data: {
|
||||
title: 'child doc',
|
||||
slug: 'child-doc',
|
||||
parent: parentDoc.id,
|
||||
_status: 'published',
|
||||
},
|
||||
})
|
||||
|
||||
// expect breadcrumbs to be an array
|
||||
expect(childDoc.breadcrumbs).toBeInstanceOf(Array)
|
||||
expect(childDoc.breadcrumbs).toBeDefined()
|
||||
|
||||
// expect each to be objects
|
||||
childDoc.breadcrumbs?.map((breadcrumb) => {
|
||||
expect(breadcrumb).toBeInstanceOf(Object)
|
||||
})
|
||||
})
|
||||
|
||||
it('should update child doc breadcrumb without affecting any other data', async () => {
|
||||
const parentDoc = await payload.create({
|
||||
collection: 'pages',
|
||||
data: {
|
||||
title: 'parent doc',
|
||||
slug: 'parent',
|
||||
},
|
||||
})
|
||||
|
||||
const childDoc = await payload.create({
|
||||
collection: 'pages',
|
||||
data: {
|
||||
title: 'child doc',
|
||||
slug: 'child',
|
||||
parent: parentDoc.id,
|
||||
_status: 'published',
|
||||
},
|
||||
})
|
||||
|
||||
await payload.update({
|
||||
collection: 'pages',
|
||||
id: parentDoc.id,
|
||||
data: {
|
||||
title: 'parent updated',
|
||||
slug: 'parent-updated',
|
||||
_status: 'published',
|
||||
},
|
||||
})
|
||||
|
||||
const updatedChild = await payload
|
||||
.find({
|
||||
collection: 'pages',
|
||||
where: {
|
||||
id: {
|
||||
equals: childDoc.id,
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(({ docs }) => docs[0])
|
||||
|
||||
if (!updatedChild) {
|
||||
return
|
||||
}
|
||||
|
||||
// breadcrumbs should be updated
|
||||
expect(updatedChild.breadcrumbs).toHaveLength(2)
|
||||
|
||||
expect(updatedChild.breadcrumbs?.[0]?.url).toStrictEqual('/parent-updated')
|
||||
expect(updatedChild.breadcrumbs?.[1]?.url).toStrictEqual('/parent-updated/child')
|
||||
|
||||
// no other data should be affected
|
||||
expect(updatedChild.title).toEqual('child doc')
|
||||
expect(updatedChild.slug).toEqual('child')
|
||||
})
|
||||
})
|
||||
|
||||
describe('overrides', () => {
|
||||
|
||||
@@ -1648,7 +1648,10 @@ describe('Select', () => {
|
||||
},
|
||||
})
|
||||
|
||||
expect(Object.keys(res)).toStrictEqual(['id', 'text'])
|
||||
expect(res).toStrictEqual({
|
||||
id: res.id,
|
||||
text: res.text,
|
||||
})
|
||||
})
|
||||
|
||||
it('should apply select with updateByID', async () => {
|
||||
@@ -1661,7 +1664,10 @@ describe('Select', () => {
|
||||
select: { text: true },
|
||||
})
|
||||
|
||||
expect(Object.keys(res)).toStrictEqual(['id', 'text'])
|
||||
expect(res).toStrictEqual({
|
||||
id: res.id,
|
||||
text: res.text,
|
||||
})
|
||||
})
|
||||
|
||||
it('should apply select with updateBulk', async () => {
|
||||
@@ -1680,7 +1686,10 @@ describe('Select', () => {
|
||||
|
||||
assert(res.docs[0])
|
||||
|
||||
expect(Object.keys(res.docs[0])).toStrictEqual(['id', 'text'])
|
||||
expect(res.docs[0]).toStrictEqual({
|
||||
id: res.docs[0].id,
|
||||
text: res.docs[0].text,
|
||||
})
|
||||
})
|
||||
|
||||
it('should apply select with deleteByID', async () => {
|
||||
@@ -1692,7 +1701,10 @@ describe('Select', () => {
|
||||
select: { text: true },
|
||||
})
|
||||
|
||||
expect(Object.keys(res)).toStrictEqual(['id', 'text'])
|
||||
expect(res).toStrictEqual({
|
||||
id: res.id,
|
||||
text: res.text,
|
||||
})
|
||||
})
|
||||
|
||||
it('should apply select with deleteBulk', async () => {
|
||||
@@ -1710,7 +1722,10 @@ describe('Select', () => {
|
||||
|
||||
assert(res.docs[0])
|
||||
|
||||
expect(Object.keys(res.docs[0])).toStrictEqual(['id', 'text'])
|
||||
expect(res.docs[0]).toStrictEqual({
|
||||
id: res.docs[0].id,
|
||||
text: res.docs[0].text,
|
||||
})
|
||||
})
|
||||
|
||||
it('should apply select with duplicate', async () => {
|
||||
@@ -1722,7 +1737,10 @@ describe('Select', () => {
|
||||
select: { text: true },
|
||||
})
|
||||
|
||||
expect(Object.keys(res)).toStrictEqual(['id', 'text'])
|
||||
expect(res).toStrictEqual({
|
||||
id: res.id,
|
||||
text: res.text,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ export const Uploads1: CollectionConfig = {
|
||||
relationTo: 'uploads-2',
|
||||
filterOptions: {
|
||||
mimeType: {
|
||||
equals: 'image/png',
|
||||
in: ['image/png', 'application/pdf'],
|
||||
},
|
||||
},
|
||||
hasMany: true,
|
||||
|
||||
@@ -65,6 +65,9 @@ let uploadsOne: AdminUrlUtil
|
||||
let uploadsTwo: AdminUrlUtil
|
||||
let customUploadFieldURL: AdminUrlUtil
|
||||
let hideFileInputOnCreateURL: AdminUrlUtil
|
||||
let consoleErrorsFromPage: string[] = []
|
||||
let collectErrorsFromPage: () => boolean
|
||||
let stopCollectingErrorsFromPage: () => boolean
|
||||
|
||||
describe('Uploads', () => {
|
||||
let page: Page
|
||||
@@ -99,7 +102,14 @@ describe('Uploads', () => {
|
||||
const context = await browser.newContext()
|
||||
page = await context.newPage()
|
||||
|
||||
initPageConsoleErrorCatch(page, { ignoreCORS: true })
|
||||
const { consoleErrors, collectErrors, stopCollectingErrors } = initPageConsoleErrorCatch(page, {
|
||||
ignoreCORS: true,
|
||||
})
|
||||
|
||||
consoleErrorsFromPage = consoleErrors
|
||||
collectErrorsFromPage = collectErrors
|
||||
stopCollectingErrorsFromPage = stopCollectingErrors
|
||||
|
||||
await ensureCompilationIsDone({ page, serverURL })
|
||||
})
|
||||
|
||||
@@ -744,6 +754,55 @@ describe('Uploads', () => {
|
||||
await saveDocAndAssert(page)
|
||||
})
|
||||
|
||||
test('should bulk upload non-image files without page errors', async () => {
|
||||
// Enable collection ONLY for this test
|
||||
collectErrorsFromPage()
|
||||
|
||||
// Navigate to the upload creation page
|
||||
await page.goto(uploadsOne.create)
|
||||
await page.waitForURL(uploadsOne.create)
|
||||
|
||||
// Upload single file
|
||||
await page.setInputFiles(
|
||||
'.file-field input[type="file"]',
|
||||
path.resolve(dirname, './image.png'),
|
||||
)
|
||||
const filename = page.locator('.file-field__filename')
|
||||
await expect(filename).toHaveValue('image.png')
|
||||
|
||||
const bulkUploadButton = page.locator('#field-hasManyUpload button', {
|
||||
hasText: exactText('Create New'),
|
||||
})
|
||||
await bulkUploadButton.click()
|
||||
|
||||
const bulkUploadModal = page.locator('#bulk-upload-drawer-slug-1')
|
||||
await expect(bulkUploadModal).toBeVisible()
|
||||
|
||||
await page.setInputFiles('#bulk-upload-drawer-slug-1 .dropzone input[type="file"]', [
|
||||
path.resolve(dirname, './test-pdf.pdf'),
|
||||
])
|
||||
|
||||
await page
|
||||
.locator('.bulk-upload--file-manager .render-fields #field-prefix')
|
||||
.fill('prefix-one')
|
||||
const saveButton = page.locator('.bulk-upload--actions-bar__saveButtons button')
|
||||
await saveButton.click()
|
||||
|
||||
await page.waitForSelector('#field-hasManyUpload .upload--has-many__dragItem')
|
||||
const itemCount = await page
|
||||
.locator('#field-hasManyUpload .upload--has-many__dragItem')
|
||||
.count()
|
||||
expect(itemCount).toEqual(1)
|
||||
|
||||
await saveDocAndAssert(page)
|
||||
|
||||
// Assert no console errors occurred for this test only
|
||||
expect(consoleErrorsFromPage).toEqual([])
|
||||
|
||||
// Reset global behavior for other tests
|
||||
stopCollectingErrorsFromPage()
|
||||
})
|
||||
|
||||
test('should apply field value to all bulk upload files after edit many', async () => {
|
||||
// Navigate to the upload creation page
|
||||
await page.goto(uploadsOne.create)
|
||||
|
||||
@@ -64,7 +64,6 @@ export interface Config {
|
||||
auth: {
|
||||
users: UserAuthOperations;
|
||||
};
|
||||
blocks: {};
|
||||
collections: {
|
||||
relation: Relation;
|
||||
audio: Audio;
|
||||
|
||||
BIN
test/uploads/test-pdf.pdf
Normal file
BIN
test/uploads/test-pdf.pdf
Normal file
Binary file not shown.
@@ -438,6 +438,47 @@ describe('Versions', () => {
|
||||
await expect(drawer.locator('.id-label')).toBeVisible()
|
||||
})
|
||||
|
||||
test('collection - autosave - should not create duplicates when clicking Create new', async () => {
|
||||
// This test checks that when we click "Create new" in the list view, it only creates 1 extra document and not more
|
||||
const { totalDocs: initialDocsCount } = await payload.find({
|
||||
collection: autosaveCollectionSlug,
|
||||
draft: true,
|
||||
})
|
||||
|
||||
await page.goto(autosaveURL.create)
|
||||
await page.locator('#field-title').fill('autosave title')
|
||||
await waitForAutoSaveToRunAndComplete(page)
|
||||
await expect(page.locator('#field-title')).toHaveValue('autosave title')
|
||||
|
||||
const { totalDocs: updatedDocsCount } = await payload.find({
|
||||
collection: autosaveCollectionSlug,
|
||||
draft: true,
|
||||
})
|
||||
|
||||
await expect(() => {
|
||||
expect(updatedDocsCount).toBe(initialDocsCount + 1)
|
||||
}).toPass({ timeout: POLL_TOPASS_TIMEOUT, intervals: [100] })
|
||||
|
||||
await page.goto(autosaveURL.list)
|
||||
const createNewButton = page.locator('.list-header .btn:has-text("Create New")')
|
||||
await createNewButton.click()
|
||||
|
||||
await page.waitForURL(`**/${autosaveCollectionSlug}/**`)
|
||||
|
||||
await page.locator('#field-title').fill('autosave title')
|
||||
await waitForAutoSaveToRunAndComplete(page)
|
||||
await expect(page.locator('#field-title')).toHaveValue('autosave title')
|
||||
|
||||
const { totalDocs: latestDocsCount } = await payload.find({
|
||||
collection: autosaveCollectionSlug,
|
||||
draft: true,
|
||||
})
|
||||
|
||||
await expect(() => {
|
||||
expect(latestDocsCount).toBe(updatedDocsCount + 1)
|
||||
}).toPass({ timeout: POLL_TOPASS_TIMEOUT, intervals: [100] })
|
||||
})
|
||||
|
||||
test('collection - should update updatedAt', async () => {
|
||||
await page.goto(url.create)
|
||||
await page.waitForURL(`**/${url.create}`)
|
||||
@@ -757,7 +798,7 @@ describe('Versions', () => {
|
||||
|
||||
// schedule publish should not be available before document has been saved
|
||||
await page.locator('#action-save-popup').click()
|
||||
await expect(page.locator('#schedule-publish')).not.toBeVisible()
|
||||
await expect(page.locator('#schedule-publish')).toBeHidden()
|
||||
|
||||
// save draft then try to schedule publish
|
||||
await saveDocAndAssert(page)
|
||||
|
||||
Reference in New Issue
Block a user