Merge pull request #5466 from payloadcms/temp20

chore: improve e2e and int test speed, reduce flakiness and errors
This commit is contained in:
Alessio Gravili
2024-03-26 00:13:50 -04:00
committed by GitHub
16 changed files with 338 additions and 278 deletions

View File

@@ -126,6 +126,8 @@ export default buildConfigWithDefaults({
],
},
onInit: async (payload) => {
await clearAndSeedEverything(payload)
if (process.env.SEED_IN_CONFIG_ONINIT !== 'false') {
await clearAndSeedEverything(payload)
}
},
})

View File

@@ -68,6 +68,7 @@ describe('admin', () => {
let serverURL: string
beforeAll(async ({ browser }) => {
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 initPayloadE2E({ config, dirname }))
geoUrl = new AdminUrlUtil(serverURL, geoCollectionSlug)
url = new AdminUrlUtil(serverURL, postsCollectionSlug)

View File

@@ -1,6 +1,7 @@
import type { Payload } from 'payload'
import { devUser } from '../credentials.js'
import { executePromises } from '../helpers/executePromises.js'
import { seedDB } from '../helpers/seed.js'
import {
collectionSlugs,
@@ -13,73 +14,102 @@ import {
usersCollectionSlug,
} from './slugs.js'
export async function clearAndSeedEverything(_payload: Payload) {
export async function clearAndSeedEverything(_payload: Payload, parallel: boolean = false) {
return await seedDB({
snapshotKey: 'adminTest',
shouldResetDB: true,
collectionSlugs,
_payload,
seedFunction: async (_payload) => {
await Promise.all([
_payload.create({
collection: usersCollectionSlug,
data: {
email: devUser.email,
password: devUser.password,
},
}),
...[...Array(11)].map(() => {
void _payload.create({
collection: postsCollectionSlug,
data: {
title: 'Title',
description: 'Description',
},
})
}),
_payload.create({
collection: customViews1CollectionSlug,
data: {
title: 'Custom View',
},
}),
_payload.create({
collection: customViews2CollectionSlug,
data: {
title: 'Custom View',
},
}),
_payload.create({
collection: geoCollectionSlug,
data: {
point: [7, -7],
},
}),
_payload.create({
collection: geoCollectionSlug,
data: {
point: [5, -5],
},
}),
_payload.create({
collection: noApiViewCollectionSlug,
data: {},
}),
_payload.create({
collection: 'customIdTab',
data: {
id: customIdCollectionId,
title: 'Hello world title',
},
}),
_payload.create({
collection: 'customIdRow',
data: {
id: customIdCollectionId,
title: 'Hello world title',
},
}),
])
await executePromises(
[
() =>
_payload.create({
collection: usersCollectionSlug,
data: {
email: devUser.email,
password: devUser.password,
},
depth: 0,
overrideAccess: true,
}),
...[...Array(11)].map(
() => () =>
_payload.create({
collection: postsCollectionSlug,
data: {
title: 'Title',
description: 'Description',
},
depth: 0,
overrideAccess: true,
}),
),
() =>
_payload.create({
collection: customViews1CollectionSlug,
data: {
title: 'Custom View',
},
depth: 0,
overrideAccess: true,
}),
() =>
_payload.create({
collection: customViews2CollectionSlug,
data: {
title: 'Custom View',
},
depth: 0,
overrideAccess: true,
}),
() =>
_payload.create({
collection: geoCollectionSlug,
data: {
point: [7, -7],
},
depth: 0,
overrideAccess: true,
}),
() =>
_payload.create({
collection: geoCollectionSlug,
data: {
point: [5, -5],
},
depth: 0,
overrideAccess: true,
}),
() =>
_payload.create({
collection: noApiViewCollectionSlug,
data: {},
depth: 0,
overrideAccess: true,
}),
() =>
_payload.create({
collection: 'customIdTab',
data: {
id: customIdCollectionId,
title: 'Hello world title',
},
depth: 0,
overrideAccess: true,
}),
() =>
_payload.create({
collection: 'customIdRow',
data: {
id: customIdCollectionId,
title: 'Hello world title',
},
depth: 0,
overrideAccess: true,
}),
],
parallel,
)
},
})
}

View File

@@ -78,6 +78,8 @@ export default buildConfigWithDefaults({
locales: ['en', 'es'],
},
onInit: async (payload) => {
await clearAndSeedEverything(payload)
if (process.env.SEED_IN_CONFIG_ONINIT !== 'false') {
await clearAndSeedEverything(payload)
}
},
})

View File

@@ -42,18 +42,18 @@ let serverURL: string
describe('fields', () => {
beforeAll(async ({ browser }) => {
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 initPayloadE2E({ config, dirname }))
client = new RESTClient(null, { defaultSlug: 'users', serverURL })
await client.login()
const context = await browser.newContext()
page = await context.newPage()
initPageConsoleErrorCatch(page)
})
beforeEach(async () => {
await clearAndSeedEverything(payload)
await client.logout()
if (client) {
await client.logout()
}
client = new RESTClient(null, { defaultSlug: 'users', serverURL })
await client.login()
})

View File

@@ -41,20 +41,8 @@ import { initPayloadInt } from '../helpers/initPayloadInt.js'
describe('Fields', () => {
beforeAll(async () => {
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, restClient } = await initPayloadInt(configPromise))
await restClient.login({
slug: 'users',
credentials: devUser,
})
user = await payload.login({
collection: 'users',
data: {
email: devUser.email,
password: devUser.password,
},
})
})
afterAll(async () => {
@@ -69,6 +57,14 @@ describe('Fields', () => {
slug: 'users',
credentials: devUser,
})
user = await payload.login({
collection: 'users',
data: {
email: devUser.email,
password: devUser.password,
},
})
})
describe('text', () => {

View File

@@ -31,14 +31,19 @@ let serverURL: string
async function navigateToLexicalFields() {
const url: AdminUrlUtil = new AdminUrlUtil(serverURL, 'lexical-fields')
await page.goto(url.list)
await page.locator('.row-1 .cell-title a').click()
const linkToDoc = page.locator('tbody tr:first-child .cell-title a').first()
await expect(() => expect(linkToDoc).toBeTruthy()).toPass({ timeout: 45000 })
const linkDocHref = await linkToDoc.getAttribute('href')
await linkToDoc.click()
await page.waitForURL(`**${linkDocHref}`)
}
describe('lexical', () => {
beforeAll(async ({ browser }) => {
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 initPayloadE2E({ config, dirname }))
client = new RESTClient(null, { defaultSlug: 'rich-text-fields', serverURL })
await client.login()
const context = await browser.newContext()
page = await context.newPage()
@@ -47,7 +52,9 @@ describe('lexical', () => {
})
beforeEach(async () => {
await clearAndSeedEverything(payload)
await client.logout()
if (client) {
await client.logout()
}
client = new RESTClient(null, { defaultSlug: 'rich-text-fields', serverURL })
await client.login()
})

View File

@@ -44,13 +44,14 @@ let createdRichTextDocID: number | string = null
describe('Lexical', () => {
beforeAll(async () => {
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
const config = await startMemoryDB(configPromise)
payload = await getPayload({ config })
restClient = new NextRESTClient(payload.config)
})
beforeEach(async () => {
await clearAndSeedEverything(payload)
restClient = new NextRESTClient(payload.config)
await restClient.login({
slug: 'users',
credentials: devUser,

View File

@@ -185,135 +185,137 @@ export async function clearAndSeedEverything(_payload: Payload, parallel: boolea
.replace(/"\{\{RICH_TEXT_DOC_ID\}\}"/g, `${formattedRichTextDocID}`),
)
await executePromises([
() =>
_payload.create({
collection: usersSlug,
depth: 0,
data: {
email: devUser.email,
password: devUser.password,
},
overrideAccess: true,
}),
() =>
_payload.create({
collection: collapsibleFieldsSlug,
data: collapsibleDoc,
depth: 0,
overrideAccess: true,
}),
() =>
_payload.create({
collection: conditionalLogicSlug,
data: conditionalLogicDoc,
depth: 0,
overrideAccess: true,
}),
() =>
_payload.create({
collection: groupFieldsSlug,
data: groupDoc,
depth: 0,
overrideAccess: true,
}),
() =>
_payload.create({
collection: selectFieldsSlug,
data: selectsDoc,
depth: 0,
overrideAccess: true,
}),
() =>
_payload.create({
collection: radioFieldsSlug,
data: radiosDoc,
depth: 0,
overrideAccess: true,
}),
() =>
_payload.create({
collection: tabsFieldsSlug,
data: tabsDoc,
depth: 0,
overrideAccess: true,
}),
() =>
_payload.create({
collection: pointFieldsSlug,
data: pointDoc,
depth: 0,
overrideAccess: true,
}),
() =>
_payload.create({
collection: dateFieldsSlug,
data: dateDoc,
depth: 0,
overrideAccess: true,
}),
() =>
_payload.create({
collection: codeFieldsSlug,
data: codeDoc,
depth: 0,
overrideAccess: true,
}),
() =>
_payload.create({
collection: jsonFieldsSlug,
data: jsonDoc,
depth: 0,
overrideAccess: true,
}),
await executePromises(
[
() =>
_payload.create({
collection: usersSlug,
depth: 0,
data: {
email: devUser.email,
password: devUser.password,
},
overrideAccess: true,
}),
() =>
_payload.create({
collection: collapsibleFieldsSlug,
data: collapsibleDoc,
depth: 0,
overrideAccess: true,
}),
() =>
_payload.create({
collection: conditionalLogicSlug,
data: conditionalLogicDoc,
depth: 0,
overrideAccess: true,
}),
() =>
_payload.create({
collection: groupFieldsSlug,
data: groupDoc,
depth: 0,
overrideAccess: true,
}),
() =>
_payload.create({
collection: selectFieldsSlug,
data: selectsDoc,
depth: 0,
overrideAccess: true,
}),
() =>
_payload.create({
collection: radioFieldsSlug,
data: radiosDoc,
depth: 0,
overrideAccess: true,
}),
() =>
_payload.create({
collection: tabsFieldsSlug,
data: tabsDoc,
depth: 0,
overrideAccess: true,
}),
() =>
_payload.create({
collection: pointFieldsSlug,
data: pointDoc,
depth: 0,
overrideAccess: true,
}),
() =>
_payload.create({
collection: dateFieldsSlug,
data: dateDoc,
depth: 0,
overrideAccess: true,
}),
() =>
_payload.create({
collection: codeFieldsSlug,
data: codeDoc,
depth: 0,
overrideAccess: true,
}),
() =>
_payload.create({
collection: jsonFieldsSlug,
data: jsonDoc,
depth: 0,
overrideAccess: true,
}),
() =>
_payload.create({
collection: blockFieldsSlug,
data: blocksDocWithRichText,
depth: 0,
overrideAccess: true,
}),
() =>
_payload.create({
collection: blockFieldsSlug,
data: blocksDocWithRichText,
depth: 0,
overrideAccess: true,
}),
() =>
_payload.create({
collection: lexicalFieldsSlug,
data: lexicalDocWithRelId,
depth: 0,
overrideAccess: true,
}),
() =>
_payload.create({
collection: lexicalMigrateFieldsSlug,
data: lexicalMigrateDocWithRelId,
depth: 0,
overrideAccess: true,
}),
() =>
_payload.create({
collection: lexicalFieldsSlug,
data: lexicalDocWithRelId,
depth: 0,
overrideAccess: true,
}),
() =>
_payload.create({
collection: lexicalMigrateFieldsSlug,
data: lexicalMigrateDocWithRelId,
depth: 0,
overrideAccess: true,
}),
() =>
_payload.create({
collection: numberFieldsSlug,
data: { number: 2 },
depth: 0,
overrideAccess: true,
}),
() =>
_payload.create({
collection: numberFieldsSlug,
data: { number: 3 },
depth: 0,
overrideAccess: true,
}),
() =>
_payload.create({
collection: numberFieldsSlug,
data: numberDoc,
depth: 0,
overrideAccess: true,
}),
])
() =>
_payload.create({
collection: numberFieldsSlug,
data: { number: 2 },
depth: 0,
overrideAccess: true,
}),
() =>
_payload.create({
collection: numberFieldsSlug,
data: { number: 3 },
depth: 0,
overrideAccess: true,
}),
() =>
_payload.create({
collection: numberFieldsSlug,
data: numberDoc,
depth: 0,
overrideAccess: true,
}),
],
parallel,
)
},
shouldResetDB: true,
snapshotKey: 'fieldsTest',
uploadsDir: path.resolve(dirname, './collections/Upload/uploads'),
})

View File

@@ -3,7 +3,7 @@
*/
export async function executePromises<T extends Array<() => Promise<any>>>(
promiseFns: T,
parallel: boolean = false,
parallel: boolean,
): Promise<{ [K in keyof T]: Awaited<ReturnType<T[K]>> }> {
if (parallel) {
// Parallel execution with Promise.all and maintain proper typing

View File

@@ -12,14 +12,12 @@ export async function seedDB({
_payload,
collectionSlugs,
seedFunction,
shouldResetDB,
snapshotKey,
uploadsDir,
}: {
_payload: Payload
collectionSlugs: string[]
seedFunction: SeedFunction
shouldResetDB: boolean
/**
* Key to uniquely identify the kind of snapshot. Each test suite should pass in a unique key
*/
@@ -27,30 +25,9 @@ export async function seedDB({
uploadsDir?: string
}) {
/**
* Reset database and delete uploads directory
* Reset database
*/
if (shouldResetDB) {
let clearUploadsDirPromise: any = Promise.resolve()
if (uploadsDir) {
clearUploadsDirPromise = fs.promises
.access(uploadsDir)
.then(() => fs.promises.readdir(uploadsDir))
.then((files) =>
Promise.all(files.map((file) => fs.promises.rm(path.join(uploadsDir, file)))),
)
.catch((error) => {
if (error.code !== 'ENOENT') {
// If the error is not because the directory doesn't exist
console.error('Error clearing the uploads directory:', error)
throw error
}
// If the directory does not exist, resolve the promise (nothing to clear)
return
})
}
await Promise.all([resetDB(_payload, collectionSlugs), clearUploadsDirPromise])
}
await resetDB(_payload, collectionSlugs)
/**
* Mongoose & Postgres: Restore snapshot of old data if available
@@ -69,14 +46,12 @@ export async function seedDB({
* Postgres: No need for any action here, since we only delete the table data and no schemas
*/
// Dropping the db breaks indexes (on mongoose - did not test extensively on postgres yet), so we recreate them here
if (shouldResetDB) {
if (isMongoose(_payload)) {
await Promise.all([
...collectionSlugs.map(async (collectionSlug) => {
await _payload.db.collections[collectionSlug].createIndexes()
}),
])
}
if (isMongoose(_payload)) {
await Promise.all([
...collectionSlugs.map(async (collectionSlug) => {
await _payload.db.collections[collectionSlug].createIndexes()
}),
])
}
/**
@@ -86,6 +61,29 @@ export async function seedDB({
return
}
/**
* Delete uploads directory only if no snapshot was restored.
* The snapshot restoration only restores the database state, not the uploads directory.
* If we ran it after or before restoring the snapshot, we would have NO upload files anymore, as they are not restored from the snapshot. And after snapshot
* restoration the seed process is not run again
*/
if (uploadsDir) {
try {
// Attempt to clear the uploads directory if it exists
await fs.promises.access(uploadsDir)
const files = await fs.promises.readdir(uploadsDir)
for (const file of files) {
await fs.promises.rm(path.join(uploadsDir, file))
}
} catch (error) {
if (error.code !== 'ENOENT') {
// If the error is not because the directory doesn't exist
console.error('Error in operation:', error)
throw error
}
}
}
/**
* Seed the database with data and save it to a snapshot
**/

View File

@@ -28,9 +28,9 @@ describe('Live Preview', () => {
const linkToDoc = page.locator('tbody tr:first-child .cell-slug a').first()
await expect(() => expect(linkToDoc).toBeTruthy()).toPass({ timeout: 45000 })
const linkDocHref = await linkToDoc.getAttribute('href')
await linkToDoc.click()
const linkDocHref = await linkToDoc.getAttribute('href')
await page.waitForURL(`**${linkDocHref}`)
}

View File

@@ -19,6 +19,8 @@ export default buildConfigWithDefaults({
locales: ['en', 'es'],
},
onInit: async (payload) => {
await clearAndSeedEverything(payload)
if (process.env.SEED_IN_CONFIG_ONINIT !== 'false') {
await clearAndSeedEverything(payload)
}
},
})

View File

@@ -72,6 +72,7 @@ describe('versions', () => {
let postURL: AdminUrlUtil
beforeAll(async ({ browser }) => {
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 initPayloadE2E({ config, dirname }))
const context = await browser.newContext()
page = await context.newPage()

View File

@@ -35,7 +35,18 @@ const formatGraphQLID = (id: number | string) =>
describe('Versions', () => {
beforeAll(async () => {
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, restClient } = await initPayloadInt(configPromise))
})
afterAll(async () => {
if (typeof payload.db.destroy === 'function') {
await payload.db.destroy()
}
})
beforeEach(async () => {
await clearAndSeedEverything(payload)
const login = `
mutation {
@@ -51,16 +62,6 @@ describe('Versions', () => {
.then((res) => res.json())
token = data.loginUser.token
})
afterAll(async () => {
if (typeof payload.db.destroy === 'function') {
await payload.db.destroy()
}
})
beforeEach(async () => {
await clearAndSeedEverything(payload)
// now: initialize
const autosavePost = await payload.create({

View File

@@ -1,14 +1,14 @@
import { type Payload } from 'payload'
import { devUser } from '../credentials.js'
import { executePromises } from '../helpers/executePromises.js'
import { seedDB } from '../helpers/seed.js'
import { titleToDelete } from './shared.js'
import { collectionSlugs, draftCollectionSlug } from './slugs.js'
export async function clearAndSeedEverything(_payload: Payload) {
export async function clearAndSeedEverything(_payload: Payload, parallel: boolean = false) {
return await seedDB({
snapshotKey: 'versionsTest',
shouldResetDB: true,
collectionSlugs,
_payload,
seedFunction: async (_payload) => {
@@ -20,25 +20,34 @@ export async function clearAndSeedEverything(_payload: Payload) {
},
]
await Promise.all([
_payload.create({
collection: 'users',
data: {
email: devUser.email,
password: devUser.password,
},
}),
_payload.create({
collection: draftCollectionSlug,
data: {
blocksField,
description: 'Description',
radio: 'test',
title: 'Draft Title',
},
draft: true,
}),
])
await executePromises(
[
() =>
_payload.create({
collection: 'users',
data: {
email: devUser.email,
password: devUser.password,
},
depth: 0,
overrideAccess: true,
}),
() =>
_payload.create({
collection: draftCollectionSlug,
data: {
blocksField,
description: 'Description',
radio: 'test',
title: 'Draft Title',
},
depth: 0,
overrideAccess: true,
draft: true,
}),
],
parallel,
)
const { id: manyDraftsID } = await _payload.create({
collection: draftCollectionSlug,
@@ -48,6 +57,8 @@ export async function clearAndSeedEverything(_payload: Payload) {
radio: 'test',
title: 'Title With Many Versions',
},
depth: 0,
overrideAccess: true,
draft: true,
})
@@ -58,6 +69,8 @@ export async function clearAndSeedEverything(_payload: Payload) {
data: {
title: `Title With Many Versions ${i + 2}`,
},
depth: 0,
overrideAccess: true,
})
}
@@ -70,6 +83,8 @@ export async function clearAndSeedEverything(_payload: Payload) {
radio: 'test',
title: 'Published Title',
},
depth: 0,
overrideAccess: true,
draft: false,
})
@@ -80,6 +95,8 @@ export async function clearAndSeedEverything(_payload: Payload) {
description: 'Description',
title: titleToDelete,
},
depth: 0,
overrideAccess: true,
draft: true,
})
},