chore: emails e2e suite (#5698)
fix: move 'email' configuration to server only
This commit is contained in:
1
.github/workflows/main.yml
vendored
1
.github/workflows/main.yml
vendored
@@ -248,6 +248,7 @@ jobs:
|
|||||||
- access-control
|
- access-control
|
||||||
# - admin
|
# - admin
|
||||||
- auth
|
- auth
|
||||||
|
- email
|
||||||
- field-error-states
|
- field-error-states
|
||||||
- fields-relationship
|
- fields-relationship
|
||||||
# - fields
|
# - fields
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export type ServerOnlyRootProperties = keyof Pick<
|
|||||||
| 'csrf'
|
| 'csrf'
|
||||||
| 'db'
|
| 'db'
|
||||||
| 'editor'
|
| 'editor'
|
||||||
|
| 'email'
|
||||||
| 'endpoints'
|
| 'endpoints'
|
||||||
| 'hooks'
|
| 'hooks'
|
||||||
| 'onInit'
|
| 'onInit'
|
||||||
@@ -59,6 +60,7 @@ export const createClientConfig = async (
|
|||||||
'typescript',
|
'typescript',
|
||||||
'cors',
|
'cors',
|
||||||
'csrf',
|
'csrf',
|
||||||
|
'email',
|
||||||
// `admin`, `onInit`, `localization`, `collections`, and `globals` are all handled separately
|
// `admin`, `onInit`, `localization`, `collections`, and `globals` are all handled separately
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
8
test/email/.eslintrc.cjs
Normal file
8
test/email/.eslintrc.cjs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import('eslint').Linter.Config} */
|
||||||
|
module.exports = {
|
||||||
|
ignorePatterns: ['payload-types.ts'],
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.eslint.json'],
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
},
|
||||||
|
}
|
||||||
2
test/email/.gitignore
vendored
Normal file
2
test/email/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/media
|
||||||
|
/media-gif
|
||||||
35
test/email/collections/Media/index.ts
Normal file
35
test/email/collections/Media/index.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import type { CollectionConfig } from 'payload/types'
|
||||||
|
|
||||||
|
import { getPayload } from 'payload'
|
||||||
|
|
||||||
|
export const mediaSlug = 'media'
|
||||||
|
|
||||||
|
export const MediaCollection: CollectionConfig = {
|
||||||
|
slug: mediaSlug,
|
||||||
|
access: {
|
||||||
|
create: () => true,
|
||||||
|
read: () => true,
|
||||||
|
},
|
||||||
|
fields: [],
|
||||||
|
upload: {
|
||||||
|
crop: true,
|
||||||
|
focalPoint: true,
|
||||||
|
imageSizes: [
|
||||||
|
{
|
||||||
|
name: 'thumbnail',
|
||||||
|
height: 200,
|
||||||
|
width: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'medium',
|
||||||
|
height: 800,
|
||||||
|
width: 800,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'large',
|
||||||
|
height: 1200,
|
||||||
|
width: 1200,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
34
test/email/collections/Posts/index.ts
Normal file
34
test/email/collections/Posts/index.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import type { CollectionConfig } from 'payload/types'
|
||||||
|
|
||||||
|
import { mediaSlug } from '../Media/index.js'
|
||||||
|
|
||||||
|
export const postsSlug = 'posts'
|
||||||
|
|
||||||
|
export const PostsCollection: CollectionConfig = {
|
||||||
|
slug: postsSlug,
|
||||||
|
admin: {
|
||||||
|
useAsTitle: 'text',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'text',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'row',
|
||||||
|
fields: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'associatedMedia',
|
||||||
|
type: 'upload',
|
||||||
|
access: {
|
||||||
|
create: () => true,
|
||||||
|
update: () => false,
|
||||||
|
},
|
||||||
|
relationTo: mediaSlug,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
versions: {
|
||||||
|
drafts: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
49
test/email/config.ts
Normal file
49
test/email/config.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import path from 'path'
|
||||||
|
import { getFileByPath } from 'payload/uploads'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
|
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||||
|
import { devUser } from '../credentials.js'
|
||||||
|
import { MediaCollection } from './collections/Media/index.js'
|
||||||
|
import { PostsCollection, postsSlug } from './collections/Posts/index.js'
|
||||||
|
import { MenuGlobal } from './globals/Menu/index.js'
|
||||||
|
|
||||||
|
const filename = fileURLToPath(import.meta.url)
|
||||||
|
const dirname = path.dirname(filename)
|
||||||
|
|
||||||
|
export default buildConfigWithDefaults({
|
||||||
|
// ...extend config here
|
||||||
|
collections: [PostsCollection, MediaCollection],
|
||||||
|
globals: [MenuGlobal],
|
||||||
|
onInit: async (payload) => {
|
||||||
|
await payload.create({
|
||||||
|
collection: 'users',
|
||||||
|
data: {
|
||||||
|
email: devUser.email,
|
||||||
|
password: devUser.password,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await payload.create({
|
||||||
|
collection: postsSlug,
|
||||||
|
data: {
|
||||||
|
text: 'example post',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const email = await payload.sendEmail({
|
||||||
|
to: 'test@example.com',
|
||||||
|
subject: 'This was sent on init',
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create image
|
||||||
|
const imageFilePath = path.resolve(dirname, '../uploads/image.png')
|
||||||
|
const imageFile = await getFileByPath(imageFilePath)
|
||||||
|
|
||||||
|
await payload.create({
|
||||||
|
collection: 'media',
|
||||||
|
data: {},
|
||||||
|
file: imageFile,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
45
test/email/e2e.spec.ts
Normal file
45
test/email/e2e.spec.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import type { Page } from '@playwright/test'
|
||||||
|
|
||||||
|
import { expect, test } from '@playwright/test'
|
||||||
|
import * as path from 'path'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
|
import type { PayloadTestSDK } from '../helpers/sdk/index.js'
|
||||||
|
import type { Config } from './payload-types.js'
|
||||||
|
|
||||||
|
import { ensureAutoLoginAndCompilationIsDone, initPageConsoleErrorCatch } from '../helpers.js'
|
||||||
|
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
|
||||||
|
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
|
||||||
|
import { POLL_TOPASS_TIMEOUT } from '../playwright.config.js'
|
||||||
|
|
||||||
|
const filename = fileURLToPath(import.meta.url)
|
||||||
|
const dirname = path.dirname(filename)
|
||||||
|
|
||||||
|
test.describe('Emails', () => {
|
||||||
|
let page: Page
|
||||||
|
let url: AdminUrlUtil
|
||||||
|
let payload: PayloadTestSDK<Config>
|
||||||
|
|
||||||
|
test.beforeAll(async ({ browser }) => {
|
||||||
|
const { payload: payloadFromConfig, serverURL } = await initPayloadE2ENoConfig({ dirname })
|
||||||
|
url = new AdminUrlUtil(serverURL, 'posts')
|
||||||
|
|
||||||
|
payload = payloadFromConfig
|
||||||
|
|
||||||
|
const context = await browser.newContext()
|
||||||
|
page = await context.newPage()
|
||||||
|
initPageConsoleErrorCatch(page)
|
||||||
|
await ensureAutoLoginAndCompilationIsDone({ page, serverURL })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('can send email via API', async () => {
|
||||||
|
const email = (await payload.sendEmail({
|
||||||
|
to: 'test@example.com',
|
||||||
|
subject: 'hello',
|
||||||
|
})) as { response: string }
|
||||||
|
|
||||||
|
await expect(() => expect(email?.response).toContain('Accepted')).toPass({
|
||||||
|
timeout: POLL_TOPASS_TIMEOUT,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
13
test/email/globals/Menu/index.ts
Normal file
13
test/email/globals/Menu/index.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import type { GlobalConfig } from 'payload/types'
|
||||||
|
|
||||||
|
export const menuSlug = 'menu'
|
||||||
|
|
||||||
|
export const MenuGlobal: GlobalConfig = {
|
||||||
|
slug: menuSlug,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'globalText',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
50
test/email/payload-types.ts
Normal file
50
test/email/payload-types.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
/* tslint:disable */
|
||||||
|
/**
|
||||||
|
* This file was automatically generated by Payload.
|
||||||
|
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
|
||||||
|
* and re-run `payload generate:types` to regenerate this file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface Config {
|
||||||
|
collections: {
|
||||||
|
posts: Post
|
||||||
|
media: Media
|
||||||
|
users: User
|
||||||
|
}
|
||||||
|
globals: {
|
||||||
|
menu: Menu
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export interface Post {
|
||||||
|
id: string
|
||||||
|
text?: string
|
||||||
|
associatedMedia?: string | Media
|
||||||
|
updatedAt: string
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
export interface Media {
|
||||||
|
id: string
|
||||||
|
updatedAt: string
|
||||||
|
createdAt: string
|
||||||
|
url?: string
|
||||||
|
filename?: string
|
||||||
|
mimeType?: string
|
||||||
|
filesize?: number
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
}
|
||||||
|
export interface User {
|
||||||
|
id: string
|
||||||
|
updatedAt: string
|
||||||
|
createdAt: string
|
||||||
|
email?: string
|
||||||
|
resetPasswordToken?: string
|
||||||
|
resetPasswordExpiration?: string
|
||||||
|
loginAttempts?: number
|
||||||
|
lockUntil?: string
|
||||||
|
password?: string
|
||||||
|
}
|
||||||
|
export interface Menu {
|
||||||
|
id: string
|
||||||
|
globalText?: string
|
||||||
|
}
|
||||||
13
test/email/tsconfig.eslint.json
Normal file
13
test/email/tsconfig.eslint.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
// extend your base config to share compilerOptions, etc
|
||||||
|
//"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
// ensure that nobody can accidentally use this config for a build
|
||||||
|
"noEmit": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
// whatever paths you intend to lint
|
||||||
|
"./**/*.ts",
|
||||||
|
"./**/*.tsx"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { SendMailOptions } from 'nodemailer'
|
||||||
import type { PaginatedDocs } from 'payload/database'
|
import type { PaginatedDocs } from 'payload/database'
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
@@ -68,6 +69,14 @@ export class PayloadTestSDK<TGeneratedTypes extends GeneratedTypes<TGeneratedTyp
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sendEmail = async ({ jwt, ...args }: { jwt?: string } & SendMailOptions): Promise<unknown> => {
|
||||||
|
return this.fetch({
|
||||||
|
method: 'sendEmail',
|
||||||
|
args,
|
||||||
|
jwt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
serverURL: string
|
serverURL: string
|
||||||
|
|
||||||
update = async <T extends keyof TGeneratedTypes['collections']>({
|
update = async <T extends keyof TGeneratedTypes['collections']>({
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export type GeneratedTypes<T extends BaseTypes> = {
|
|||||||
export type FetchOptions = {
|
export type FetchOptions = {
|
||||||
args?: Record<string, unknown>
|
args?: Record<string, unknown>
|
||||||
jwt?: string
|
jwt?: string
|
||||||
method: 'create' | 'delete' | 'find' | 'update'
|
method: 'create' | 'delete' | 'find' | 'sendEmail' | 'update'
|
||||||
reduceJSON?: <R>(json: any) => R
|
reduceJSON?: <R>(json: any) => R
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user