feat: testing refactor (e2e/int) (#748)
Co-authored-by: James <james@trbl.design> Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
This commit is contained in:
1
test/components/globalSetup.js
Normal file
1
test/components/globalSetup.js
Normal file
@@ -0,0 +1 @@
|
||||
import '@testing-library/jest-dom';
|
||||
21
test/dev/index.js
Normal file
21
test/dev/index.js
Normal file
@@ -0,0 +1,21 @@
|
||||
const path = require('path');
|
||||
const babelConfig = require('../../babel.config');
|
||||
|
||||
require('@babel/register')({
|
||||
...babelConfig,
|
||||
extensions: ['.ts', '.tsx', '.js', '.jsx'],
|
||||
env: {
|
||||
development: {
|
||||
sourceMaps: 'inline',
|
||||
retainLines: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const [testConfigDir] = process.argv.slice(2);
|
||||
|
||||
const configPath = path.resolve(__dirname, '../', testConfigDir, 'config.ts');
|
||||
process.env.PAYLOAD_CONFIG_PATH = configPath;
|
||||
process.env.PAYLOAD_DROP_DATABASE = 'true';
|
||||
|
||||
require('./server');
|
||||
32
test/dev/server.ts
Normal file
32
test/dev/server.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/* eslint-disable no-console */
|
||||
import express from 'express';
|
||||
import payload from '../../src';
|
||||
|
||||
const expressApp = express();
|
||||
|
||||
const init = async () => {
|
||||
await payload.init({
|
||||
secret: 'SECRET_KEY',
|
||||
mongoURL: process.env.MONGO_URL || 'mongodb://localhost/payload',
|
||||
express: expressApp,
|
||||
email: {
|
||||
logMockCredentials: true,
|
||||
fromName: 'Payload',
|
||||
fromAddress: 'hello@payloadcms.com',
|
||||
},
|
||||
onInit: async (app) => {
|
||||
app.logger.info('Payload Dev Server Initialized');
|
||||
},
|
||||
});
|
||||
|
||||
const externalRouter = express.Router();
|
||||
|
||||
externalRouter.use(payload.authenticate);
|
||||
|
||||
expressApp.listen(3000, async () => {
|
||||
payload.logger.info(`Admin URL on ${payload.getAdminURL()}`);
|
||||
payload.logger.info(`API URL on ${payload.getAPIURL()}`);
|
||||
});
|
||||
};
|
||||
|
||||
init();
|
||||
36
test/e2e/access-control/config.ts
Normal file
36
test/e2e/access-control/config.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { buildConfig } from '../buildConfig';
|
||||
|
||||
export const slug = 'access-controls';
|
||||
|
||||
export default buildConfig({
|
||||
collections: [
|
||||
{
|
||||
slug,
|
||||
fields: [
|
||||
{
|
||||
name: 'restrictedField',
|
||||
type: 'text',
|
||||
access: {
|
||||
read: () => false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'restricted',
|
||||
fields: [],
|
||||
access: {
|
||||
read: () => false,
|
||||
},
|
||||
},
|
||||
],
|
||||
onInit: async (payload) => {
|
||||
await payload.create({
|
||||
collection: slug,
|
||||
data: {
|
||||
restrictedField: 'restricted',
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
});
|
||||
82
test/e2e/access-control/index.spec.ts
Normal file
82
test/e2e/access-control/index.spec.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
import payload from '../../../src';
|
||||
import { AdminUrlUtil } from '../../helpers/adminUrlUtil';
|
||||
import { initPayloadE2E } from '../../helpers/configHelpers';
|
||||
import { firstRegister } from '../helpers';
|
||||
import { slug } from './config';
|
||||
|
||||
/**
|
||||
* TODO: Access Control
|
||||
* - [x] restricted collections not shown
|
||||
* - no sidebar link
|
||||
* - no route
|
||||
* - no card
|
||||
* [x] field without read access should not show
|
||||
* prevent user from logging in (canAccessAdmin)
|
||||
* no create controls if no access
|
||||
* no update control if no update
|
||||
* - check fields are rendered as readonly
|
||||
* no delete control if no access
|
||||
* no version controls is no access
|
||||
*
|
||||
* FSK: 'should properly prevent / allow public users from reading a restricted field'
|
||||
*
|
||||
* Repeat all above for globals
|
||||
*/
|
||||
|
||||
const { beforeAll, describe } = test;
|
||||
let url: AdminUrlUtil;
|
||||
|
||||
describe('access control', () => {
|
||||
let page: Page;
|
||||
|
||||
beforeAll(async ({ browser }) => {
|
||||
const { serverURL } = await initPayloadE2E(__dirname);
|
||||
// await clearDocs(); // Clear any seeded data from onInit
|
||||
|
||||
url = new AdminUrlUtil(serverURL, slug);
|
||||
|
||||
const context = await browser.newContext();
|
||||
page = await context.newPage();
|
||||
|
||||
await firstRegister({ page, serverURL });
|
||||
});
|
||||
|
||||
// afterEach(async () => {
|
||||
// });
|
||||
|
||||
test('field without read access should not show', async () => {
|
||||
const { id } = await createDoc({ restrictedField: 'restricted' });
|
||||
|
||||
await page.goto(url.doc(id));
|
||||
|
||||
await expect(page.locator('input[name="restrictedField"]')).toHaveCount(0);
|
||||
});
|
||||
|
||||
describe('restricted collection', () => {
|
||||
test('should not show in card list', async () => {
|
||||
await page.goto(url.admin);
|
||||
await expect(page.locator('.dashboard__card-list >> text=Restricteds')).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('should not show in nav', async () => {
|
||||
await page.goto(url.admin);
|
||||
await expect(page.locator('.nav >> a:has-text("Restricteds")')).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('should not have collection url', async () => {
|
||||
await page.goto(url.collection);
|
||||
await page.locator('text=Nothing found').click();
|
||||
await page.locator('a:has-text("Back to Dashboard")').click();
|
||||
await expect(page).toHaveURL(url.admin);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function createDoc(data: any): Promise<{ id: string }> {
|
||||
return payload.create({
|
||||
collection: slug,
|
||||
data,
|
||||
});
|
||||
}
|
||||
19
test/e2e/auth/config.ts
Normal file
19
test/e2e/auth/config.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { buildConfig } from '../buildConfig';
|
||||
|
||||
export const slug = 'users';
|
||||
|
||||
export default buildConfig({
|
||||
admin: {
|
||||
user: 'users',
|
||||
},
|
||||
collections: [{
|
||||
slug,
|
||||
auth: {
|
||||
verify: true,
|
||||
useAPIKey: true,
|
||||
maxLoginAttempts: 2,
|
||||
},
|
||||
fields: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
69
test/e2e/auth/index.spec.ts
Normal file
69
test/e2e/auth/index.spec.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { AdminUrlUtil } from '../../helpers/adminUrlUtil';
|
||||
import { initPayloadTest } from '../../helpers/configHelpers';
|
||||
import { firstRegister } from '../helpers';
|
||||
import { slug } from './config';
|
||||
|
||||
|
||||
/**
|
||||
* TODO: Auth
|
||||
* change password
|
||||
* unlock
|
||||
* generate api key
|
||||
* log out
|
||||
*/
|
||||
|
||||
const { beforeAll, describe } = test;
|
||||
let url: AdminUrlUtil;
|
||||
|
||||
describe('authentication', () => {
|
||||
let page: Page;
|
||||
|
||||
beforeAll(async ({ browser }) => {
|
||||
const { serverURL } = await initPayloadTest({
|
||||
__dirname,
|
||||
init: {
|
||||
local: false,
|
||||
},
|
||||
});
|
||||
// await clearDocs(); // Clear any seeded data from onInit
|
||||
|
||||
url = new AdminUrlUtil(serverURL, slug);
|
||||
|
||||
const context = await browser.newContext();
|
||||
page = await context.newPage();
|
||||
|
||||
await firstRegister({ page, serverURL });
|
||||
});
|
||||
|
||||
describe('Authentication', () => {
|
||||
test('should login and logout', () => {
|
||||
expect(1).toEqual(1);
|
||||
});
|
||||
test('should logout', () => {
|
||||
expect(1).toEqual(1);
|
||||
});
|
||||
test('should allow change password', () => {
|
||||
expect(1).toEqual(1);
|
||||
});
|
||||
test('should reset password', () => {
|
||||
expect(1).toEqual(1);
|
||||
});
|
||||
test('should lockout after reaching max login attempts', () => {
|
||||
expect(1).toEqual(1);
|
||||
});
|
||||
test('should prevent login for locked user', () => {
|
||||
expect(1).toEqual(1);
|
||||
});
|
||||
test('should unlock user', () => {
|
||||
expect(1).toEqual(1);
|
||||
});
|
||||
test('should not login without verify', () => {
|
||||
expect(1).toEqual(1);
|
||||
});
|
||||
test('should allow generate api keys', () => {
|
||||
expect(1).toEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
18
test/e2e/buildConfig.ts
Normal file
18
test/e2e/buildConfig.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import merge from 'deepmerge';
|
||||
import { buildConfig as buildPayloadConfig } from '../../src/config/build';
|
||||
import type { Config, SanitizedConfig } from '../../src/config/types';
|
||||
|
||||
export function buildConfig(overrides?: Partial<Config>): SanitizedConfig {
|
||||
const baseConfig: Config = {};
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
baseConfig.admin = {
|
||||
webpack: (config) => ({
|
||||
...config,
|
||||
cache: {
|
||||
type: 'memory',
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
return buildPayloadConfig(merge(baseConfig, overrides || {}));
|
||||
}
|
||||
39
test/e2e/collections/config.ts
Normal file
39
test/e2e/collections/config.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { mapAsync } from '../../../src/utilities/mapAsync';
|
||||
import { buildConfig } from '../buildConfig';
|
||||
|
||||
export const slug = 'posts';
|
||||
|
||||
export interface Post {
|
||||
id: string,
|
||||
title: string,
|
||||
description: string,
|
||||
createdAt: Date,
|
||||
updatedAt: Date,
|
||||
}
|
||||
|
||||
export default buildConfig({
|
||||
collections: [{
|
||||
slug,
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
}],
|
||||
onInit: async (payload) => {
|
||||
await mapAsync([...Array(11)], async () => {
|
||||
await payload.create({
|
||||
collection: slug,
|
||||
data: {
|
||||
title: 'title',
|
||||
description: 'description',
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
285
test/e2e/collections/index.spec.ts
Normal file
285
test/e2e/collections/index.spec.ts
Normal file
@@ -0,0 +1,285 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
import payload from '../../../src';
|
||||
import { AdminUrlUtil } from '../../helpers/adminUrlUtil';
|
||||
import { initPayloadTest } from '../../helpers/configHelpers';
|
||||
import { firstRegister, saveDocAndAssert } from '../helpers';
|
||||
import type { Post } from './config';
|
||||
import { slug } from './config';
|
||||
import { mapAsync } from '../../../src/utilities/mapAsync';
|
||||
import wait from '../../../src/utilities/wait';
|
||||
|
||||
const { afterEach, beforeAll, beforeEach, describe } = test;
|
||||
|
||||
const title = 'title';
|
||||
const description = 'description';
|
||||
|
||||
let url: AdminUrlUtil;
|
||||
|
||||
describe('collections', () => {
|
||||
let page: Page;
|
||||
|
||||
beforeAll(async ({ browser }) => {
|
||||
const { serverURL } = await initPayloadTest({
|
||||
__dirname,
|
||||
init: {
|
||||
local: false,
|
||||
},
|
||||
});
|
||||
await clearDocs(); // Clear any seeded data from onInit
|
||||
url = new AdminUrlUtil(serverURL, slug);
|
||||
|
||||
const context = await browser.newContext();
|
||||
page = await context.newPage();
|
||||
|
||||
await firstRegister({ page, serverURL });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await clearDocs();
|
||||
});
|
||||
|
||||
describe('Nav', () => {
|
||||
test('should nav to collection - sidebar', async () => {
|
||||
await page.goto(url.admin);
|
||||
const collectionLink = page.locator(`nav >> text=${slug}`);
|
||||
await collectionLink.click();
|
||||
|
||||
expect(page.url()).toContain(url.collection);
|
||||
});
|
||||
|
||||
test('should navigate to collection - card', async () => {
|
||||
await page.goto(url.admin);
|
||||
await page.locator('a:has-text("Posts")').click();
|
||||
expect(page.url()).toContain(url.collection);
|
||||
});
|
||||
|
||||
test('breadcrumbs - from card to dashboard', async () => {
|
||||
await page.goto(url.collection);
|
||||
await page.locator('a:has-text("Dashboard")').click();
|
||||
expect(page.url()).toContain(url.admin);
|
||||
});
|
||||
|
||||
test('breadcrumbs - from document to collection', async () => {
|
||||
const { id } = await createPost();
|
||||
|
||||
await page.goto(url.doc(id));
|
||||
await page.locator('nav >> text=Posts').click();
|
||||
expect(page.url()).toContain(url.collection);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('CRUD', () => {
|
||||
test('should create', async () => {
|
||||
await page.goto(url.create);
|
||||
await page.locator('#title').fill(title);
|
||||
await page.locator('#description').fill(description);
|
||||
await page.click('text=Save', { delay: 100 });
|
||||
|
||||
await saveDocAndAssert(page);
|
||||
|
||||
await expect(page.locator('#title')).toHaveValue(title);
|
||||
await expect(page.locator('#description')).toHaveValue(description);
|
||||
});
|
||||
|
||||
test('should read existing', async () => {
|
||||
const { id } = await createPost();
|
||||
|
||||
await page.goto(url.doc(id));
|
||||
|
||||
await expect(page.locator('#title')).toHaveValue(title);
|
||||
await expect(page.locator('#description')).toHaveValue(description);
|
||||
});
|
||||
|
||||
test('should update existing', async () => {
|
||||
const { id } = await createPost();
|
||||
|
||||
await page.goto(url.doc(id));
|
||||
|
||||
const newTitle = 'new title';
|
||||
const newDesc = 'new description';
|
||||
await page.locator('#title').fill(newTitle);
|
||||
await page.locator('#description').fill(newDesc);
|
||||
|
||||
await saveDocAndAssert(page);
|
||||
|
||||
await expect(page.locator('#title')).toHaveValue(newTitle);
|
||||
await expect(page.locator('#description')).toHaveValue(newDesc);
|
||||
});
|
||||
|
||||
test('should delete existing', async () => {
|
||||
const { id } = await createPost();
|
||||
|
||||
await page.goto(url.doc(id));
|
||||
await page.locator('button:has-text("Delete")').click();
|
||||
await page.locator('button:has-text("Confirm")').click();
|
||||
|
||||
await expect(page.locator(`text=Post "${id}" successfully deleted.`)).toBeVisible();
|
||||
expect(page.url()).toContain(url.collection);
|
||||
});
|
||||
|
||||
test('should duplicate existing', async () => {
|
||||
const { id } = await createPost();
|
||||
|
||||
await page.goto(url.doc(id));
|
||||
await page.locator('button:has-text("Duplicate")').click();
|
||||
|
||||
expect(page.url()).toContain(url.create);
|
||||
await page.locator('button:has-text("Save")').click();
|
||||
expect(page.url()).not.toContain(id); // new id
|
||||
});
|
||||
});
|
||||
|
||||
describe('list view', () => {
|
||||
const tableRowLocator = 'table >> tbody >> tr';
|
||||
|
||||
beforeEach(async () => {
|
||||
await page.goto(url.collection);
|
||||
});
|
||||
|
||||
describe('filtering', () => {
|
||||
test('search by id', async () => {
|
||||
const { id } = await createPost();
|
||||
await page.locator('.search-filter__input').fill(id);
|
||||
const tableItems = page.locator(tableRowLocator);
|
||||
await expect(tableItems).toHaveCount(1);
|
||||
});
|
||||
|
||||
test('toggle columns', async () => {
|
||||
const columnCountLocator = 'table >> thead >> tr >> th';
|
||||
await createPost();
|
||||
await page.locator('button:has-text("Columns")').click();
|
||||
await wait(1000); // Wait for column toggle UI, should probably use waitForSelector
|
||||
|
||||
const numberOfColumns = await page.locator(columnCountLocator).count();
|
||||
const idButton = page.locator('.column-selector >> text=ID');
|
||||
|
||||
// Remove ID column
|
||||
await idButton.click({ delay: 100 });
|
||||
await expect(page.locator(columnCountLocator)).toHaveCount(numberOfColumns - 1);
|
||||
|
||||
// Add back ID column
|
||||
await idButton.click({ delay: 100 });
|
||||
await expect(page.locator(columnCountLocator)).toHaveCount(numberOfColumns);
|
||||
});
|
||||
|
||||
test('filter rows', async () => {
|
||||
const { id } = await createPost({ title: 'post1' });
|
||||
await createPost({ title: 'post2' });
|
||||
|
||||
await expect(page.locator(tableRowLocator)).toHaveCount(2);
|
||||
|
||||
await page.locator('button:has-text("Filters")').click();
|
||||
await wait(1000); // Wait for column toggle UI, should probably use waitForSelector
|
||||
|
||||
await page.locator('text=Add filter').click();
|
||||
|
||||
const operatorField = page.locator('.condition >> .condition__operator');
|
||||
const valueField = page.locator('.condition >> .condition__value >> input');
|
||||
|
||||
await operatorField.click();
|
||||
|
||||
const dropdownOptions = operatorField.locator('.rs__option');
|
||||
await dropdownOptions.locator('text=equals').click();
|
||||
|
||||
await valueField.fill(id);
|
||||
await wait(1000);
|
||||
|
||||
await expect(page.locator(tableRowLocator)).toHaveCount(1);
|
||||
const firstId = await page.locator(tableRowLocator).first().locator('td').first()
|
||||
.innerText();
|
||||
expect(firstId).toEqual(id);
|
||||
|
||||
// Remove filter
|
||||
await page.locator('.condition >> .icon--x').click();
|
||||
await wait(1000);
|
||||
await expect(page.locator(tableRowLocator)).toHaveCount(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pagination', () => {
|
||||
beforeAll(async () => {
|
||||
await mapAsync([...Array(11)], async () => {
|
||||
await createPost();
|
||||
});
|
||||
});
|
||||
|
||||
test('should paginate', async () => {
|
||||
const pageInfo = page.locator('.collection-list__page-info');
|
||||
const perPage = page.locator('.per-page');
|
||||
const paginator = page.locator('.paginator');
|
||||
const tableItems = page.locator(tableRowLocator);
|
||||
|
||||
await expect(tableItems).toHaveCount(10);
|
||||
await expect(pageInfo).toHaveText('1-10 of 11');
|
||||
await expect(perPage).toContainText('Per Page: 10');
|
||||
|
||||
// Forward one page and back using numbers
|
||||
await paginator.locator('button').nth(1).click();
|
||||
expect(page.url()).toContain('?page=2');
|
||||
await expect(tableItems).toHaveCount(1);
|
||||
await paginator.locator('button').nth(0).click();
|
||||
expect(page.url()).toContain('?page=1');
|
||||
await expect(tableItems).toHaveCount(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sorting', () => {
|
||||
beforeAll(async () => {
|
||||
[1, 2].map(async () => {
|
||||
await createPost();
|
||||
});
|
||||
});
|
||||
|
||||
test('should sort', async () => {
|
||||
const getTableItems = () => page.locator(tableRowLocator);
|
||||
|
||||
await expect(getTableItems()).toHaveCount(2);
|
||||
|
||||
const chevrons = page.locator('table >> thead >> th >> button');
|
||||
const upChevron = chevrons.first();
|
||||
const downChevron = chevrons.nth(1);
|
||||
|
||||
const getFirstId = async () => getTableItems().first().locator('td').first()
|
||||
.innerText();
|
||||
const getSecondId = async () => getTableItems().nth(1).locator('td').first()
|
||||
.innerText();
|
||||
|
||||
const firstId = await getFirstId();
|
||||
const secondId = await getSecondId();
|
||||
|
||||
await upChevron.click({ delay: 100 });
|
||||
|
||||
// Order should have swapped
|
||||
expect(await getFirstId()).toEqual(secondId);
|
||||
expect(await getSecondId()).toEqual(firstId);
|
||||
|
||||
await downChevron.click({ delay: 100 });
|
||||
|
||||
// Swap back
|
||||
expect(await getFirstId()).toEqual(firstId);
|
||||
expect(await getSecondId()).toEqual(secondId);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function createPost(overrides?: Partial<Post>): Promise<Post> {
|
||||
return payload.create<Post>({
|
||||
collection: slug,
|
||||
data: {
|
||||
title,
|
||||
description,
|
||||
...overrides,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function clearDocs(): Promise<void> {
|
||||
const allDocs = await payload.find<Post>({ collection: slug, limit: 100 });
|
||||
const ids = allDocs.docs.map((doc) => doc.id);
|
||||
await mapAsync(ids, async (id) => {
|
||||
await payload.delete({ collection: slug, id });
|
||||
});
|
||||
}
|
||||
35
test/e2e/fields-array/config.ts
Normal file
35
test/e2e/fields-array/config.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { buildConfig } from '../buildConfig';
|
||||
|
||||
export const slug = 'fields-array';
|
||||
|
||||
export default buildConfig({
|
||||
collections: [
|
||||
{
|
||||
slug,
|
||||
fields: [
|
||||
{
|
||||
type: 'array',
|
||||
name: 'readOnlyArray',
|
||||
label: 'readOnly Array',
|
||||
admin: {
|
||||
readOnly: true,
|
||||
},
|
||||
defaultValue: [
|
||||
{
|
||||
text: 'defaultValue',
|
||||
},
|
||||
{
|
||||
text: 'defaultValue2',
|
||||
},
|
||||
],
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
43
test/e2e/fields-array/index.spec.ts
Normal file
43
test/e2e/fields-array/index.spec.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
import wait from '../../../src/utilities/wait';
|
||||
import { AdminUrlUtil } from '../../helpers/adminUrlUtil';
|
||||
import { initPayloadTest } from '../../helpers/configHelpers';
|
||||
import { firstRegister } from '../helpers';
|
||||
import { slug } from './config';
|
||||
|
||||
const { beforeAll, describe } = test;
|
||||
|
||||
let url: AdminUrlUtil;
|
||||
|
||||
describe('fields - array', () => {
|
||||
let page: Page;
|
||||
beforeAll(async ({ browser }) => {
|
||||
const { serverURL } = await initPayloadTest({
|
||||
__dirname,
|
||||
init: {
|
||||
local: false,
|
||||
},
|
||||
});
|
||||
url = new AdminUrlUtil(serverURL, slug);
|
||||
|
||||
const context = await browser.newContext();
|
||||
page = await context.newPage();
|
||||
|
||||
await firstRegister({ page, serverURL });
|
||||
});
|
||||
|
||||
test('should be readOnly', async () => {
|
||||
await page.goto(url.create);
|
||||
await wait(2000);
|
||||
const field = page.locator('#readOnlyArray\\.0\\.text');
|
||||
await expect(field).toBeDisabled();
|
||||
});
|
||||
|
||||
test('should have defaultValue', async () => {
|
||||
await page.goto(url.create);
|
||||
await wait(2000);
|
||||
const field = page.locator('#readOnlyArray\\.0\\.text');
|
||||
await expect(field).toHaveValue('defaultValue');
|
||||
});
|
||||
});
|
||||
142
test/e2e/fields-relationship/config.ts
Normal file
142
test/e2e/fields-relationship/config.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import type { CollectionConfig } from '../../../src/collections/config/types';
|
||||
import { buildConfig } from '../buildConfig';
|
||||
|
||||
export const slug = 'fields-relationship';
|
||||
|
||||
export const relationOneSlug = 'relation-one';
|
||||
export const relationTwoSlug = 'relation-two';
|
||||
export const relationRestrictedSlug = 'relation-restricted';
|
||||
export const relationWithTitleSlug = 'relation-with-title';
|
||||
|
||||
export interface FieldsRelationship {
|
||||
id: string;
|
||||
relationship: RelationOne;
|
||||
relationshipHasMany: RelationOne[];
|
||||
relationshipMultiple: Array<RelationOne | RelationTwo>;
|
||||
relationshipRestricted: RelationRestricted;
|
||||
relationshipWithTitle: RelationWithTitle;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface RelationOne {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
export type RelationTwo = RelationOne;
|
||||
export type RelationRestricted = RelationOne;
|
||||
export type RelationWithTitle = RelationOne;
|
||||
|
||||
const baseRelationshipFields: CollectionConfig['fields'] = [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
},
|
||||
];
|
||||
|
||||
export default buildConfig({
|
||||
collections: [
|
||||
{
|
||||
slug,
|
||||
fields: [
|
||||
{
|
||||
type: 'relationship',
|
||||
name: 'relationship',
|
||||
relationTo: relationOneSlug,
|
||||
},
|
||||
{
|
||||
type: 'relationship',
|
||||
name: 'relationshipHasMany',
|
||||
relationTo: relationOneSlug,
|
||||
hasMany: true,
|
||||
},
|
||||
{
|
||||
type: 'relationship',
|
||||
name: 'relationshipMultiple',
|
||||
relationTo: [relationOneSlug, relationTwoSlug],
|
||||
},
|
||||
{
|
||||
type: 'relationship',
|
||||
name: 'relationshipRestricted',
|
||||
relationTo: relationRestrictedSlug,
|
||||
},
|
||||
{
|
||||
type: 'relationship',
|
||||
name: 'relationshipWithTitle',
|
||||
relationTo: relationWithTitleSlug,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: relationOneSlug,
|
||||
fields: baseRelationshipFields,
|
||||
},
|
||||
{
|
||||
slug: relationTwoSlug,
|
||||
fields: baseRelationshipFields,
|
||||
},
|
||||
{
|
||||
slug: relationRestrictedSlug,
|
||||
admin: {
|
||||
useAsTitle: 'name',
|
||||
},
|
||||
fields: baseRelationshipFields,
|
||||
access: {
|
||||
read: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: relationWithTitleSlug,
|
||||
admin: {
|
||||
useAsTitle: 'name',
|
||||
},
|
||||
fields: baseRelationshipFields,
|
||||
},
|
||||
],
|
||||
onInit: async (payload) => {
|
||||
// Create docs to relate to
|
||||
const { id: relationOneDocId } = await payload.create<RelationOne>({
|
||||
collection: relationOneSlug,
|
||||
data: {
|
||||
name: relationOneSlug,
|
||||
},
|
||||
});
|
||||
|
||||
await payload.create<RelationOne>({
|
||||
collection: relationOneSlug,
|
||||
data: {
|
||||
name: relationOneSlug,
|
||||
},
|
||||
});
|
||||
|
||||
await payload.create<RelationTwo>({
|
||||
collection: relationTwoSlug,
|
||||
data: {
|
||||
name: relationTwoSlug,
|
||||
},
|
||||
});
|
||||
|
||||
// Existing relationships
|
||||
const { id: restrictedDocId } = await payload.create<RelationRestricted>({
|
||||
collection: relationRestrictedSlug,
|
||||
data: {
|
||||
name: 'relation-restricted',
|
||||
},
|
||||
});
|
||||
const { id: relationWithTitleDocId } = await payload.create<RelationWithTitle>({
|
||||
collection: relationWithTitleSlug,
|
||||
data: {
|
||||
name: 'relation-title',
|
||||
},
|
||||
});
|
||||
await payload.create<RelationOne>({
|
||||
collection: slug,
|
||||
data: {
|
||||
name: 'with-existing-relations',
|
||||
relationship: relationOneDocId,
|
||||
relationshipRestricted: restrictedDocId,
|
||||
relationshipWithTitle: relationWithTitleDocId,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
235
test/e2e/fields-relationship/index.spec.ts
Normal file
235
test/e2e/fields-relationship/index.spec.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
import payload from '../../../src';
|
||||
import { mapAsync } from '../../../src/utilities/mapAsync';
|
||||
import { AdminUrlUtil } from '../../helpers/adminUrlUtil';
|
||||
import { initPayloadTest } from '../../helpers/configHelpers';
|
||||
import { firstRegister, saveDocAndAssert } from '../helpers';
|
||||
import type {
|
||||
FieldsRelationship as CollectionWithRelationships,
|
||||
RelationOne,
|
||||
RelationRestricted,
|
||||
RelationTwo,
|
||||
RelationWithTitle,
|
||||
} from './config';
|
||||
import {
|
||||
relationOneSlug,
|
||||
relationRestrictedSlug,
|
||||
relationTwoSlug,
|
||||
relationWithTitleSlug,
|
||||
slug,
|
||||
} from './config';
|
||||
|
||||
const { beforeAll, describe } = test;
|
||||
|
||||
let url: AdminUrlUtil;
|
||||
|
||||
describe('fields - relationship', () => {
|
||||
let page: Page;
|
||||
let relationOneDoc: RelationOne;
|
||||
let anotherRelationOneDoc: RelationOne;
|
||||
let relationTwoDoc: RelationTwo;
|
||||
|
||||
let docWithExistingRelations: CollectionWithRelationships;
|
||||
let restrictedRelation: RelationRestricted;
|
||||
let relationWithTitle: RelationWithTitle;
|
||||
|
||||
beforeAll(async ({ browser }) => {
|
||||
const { serverURL } = await initPayloadTest({
|
||||
__dirname,
|
||||
init: {
|
||||
local: false,
|
||||
},
|
||||
});
|
||||
await clearAllDocs();
|
||||
|
||||
url = new AdminUrlUtil(serverURL, slug);
|
||||
|
||||
const context = await browser.newContext();
|
||||
page = await context.newPage();
|
||||
|
||||
// Create docs to relate to
|
||||
relationOneDoc = await payload.create<RelationOne>({
|
||||
collection: relationOneSlug,
|
||||
data: {
|
||||
name: 'relation',
|
||||
},
|
||||
});
|
||||
|
||||
anotherRelationOneDoc = await payload.create<RelationOne>({
|
||||
collection: relationOneSlug,
|
||||
data: {
|
||||
name: 'relation',
|
||||
},
|
||||
});
|
||||
|
||||
relationTwoDoc = await payload.create<RelationTwo>({
|
||||
collection: relationTwoSlug,
|
||||
data: {
|
||||
name: 'second-relation',
|
||||
},
|
||||
});
|
||||
|
||||
// Create restricted doc
|
||||
restrictedRelation = await payload.create<RelationRestricted>({
|
||||
collection: relationRestrictedSlug,
|
||||
data: {
|
||||
name: 'restricted',
|
||||
},
|
||||
});
|
||||
|
||||
// Doc with useAsTitle
|
||||
relationWithTitle = await payload.create<RelationWithTitle>({
|
||||
collection: relationWithTitleSlug,
|
||||
data: {
|
||||
name: 'relation-title',
|
||||
},
|
||||
});
|
||||
|
||||
// Add restricted doc as relation
|
||||
docWithExistingRelations = await payload.create<CollectionWithRelationships>({
|
||||
collection: slug,
|
||||
data: {
|
||||
name: 'with-existing-relations',
|
||||
relationship: relationOneDoc.id,
|
||||
relationshipRestricted: restrictedRelation.id,
|
||||
relationshipWithTitle: relationWithTitle.id,
|
||||
},
|
||||
});
|
||||
|
||||
await firstRegister({ page, serverURL });
|
||||
});
|
||||
|
||||
test('should create relationship', async () => {
|
||||
await page.goto(url.create);
|
||||
|
||||
const fields = page.locator('.render-fields >> .react-select');
|
||||
const relationshipField = fields.nth(0);
|
||||
|
||||
await relationshipField.click({ delay: 100 });
|
||||
|
||||
const options = page.locator('.rs__option');
|
||||
|
||||
await expect(options).toHaveCount(3); // None + two docs
|
||||
|
||||
// Select a relationship
|
||||
await options.nth(1).click();
|
||||
await expect(relationshipField).toContainText(relationOneDoc.id);
|
||||
|
||||
await saveDocAndAssert(page);
|
||||
});
|
||||
|
||||
test('should create hasMany relationship', async () => {
|
||||
await page.goto(url.create);
|
||||
|
||||
const fields = page.locator('.render-fields >> .react-select');
|
||||
const relationshipHasManyField = fields.nth(1);
|
||||
|
||||
await relationshipHasManyField.click({ delay: 100 });
|
||||
|
||||
const options = page.locator('.rs__option');
|
||||
|
||||
await expect(options).toHaveCount(2); // Two relationship options
|
||||
|
||||
// Add one relationship
|
||||
await options.locator(`text=${relationOneDoc.id}`).click();
|
||||
await expect(relationshipHasManyField).toContainText(relationOneDoc.id);
|
||||
await expect(relationshipHasManyField).not.toContainText(anotherRelationOneDoc.id);
|
||||
|
||||
// Add second relationship
|
||||
await relationshipHasManyField.click({ delay: 100 });
|
||||
await options.locator(`text=${anotherRelationOneDoc.id}`).click();
|
||||
await expect(relationshipHasManyField).toContainText(anotherRelationOneDoc.id);
|
||||
|
||||
// No options left
|
||||
await relationshipHasManyField.click({ delay: 100 });
|
||||
await expect(page.locator('.rs__menu')).toHaveText('No options');
|
||||
|
||||
await saveDocAndAssert(page);
|
||||
});
|
||||
|
||||
test('should create relations to multiple collections', async () => {
|
||||
await page.goto(url.create);
|
||||
|
||||
const fields = page.locator('.render-fields >> .react-select');
|
||||
const relationshipMultipleField = fields.nth(2);
|
||||
|
||||
await relationshipMultipleField.click({ delay: 100 });
|
||||
|
||||
const options = page.locator('.rs__option');
|
||||
|
||||
await expect(options).toHaveCount(4); // None + 3 docs
|
||||
|
||||
// Add one relationship
|
||||
await options.locator(`text=${relationOneDoc.id}`).click();
|
||||
await expect(relationshipMultipleField).toContainText(relationOneDoc.id);
|
||||
|
||||
// Add relationship of different collection
|
||||
await relationshipMultipleField.click({ delay: 100 });
|
||||
await options.locator(`text=${relationTwoDoc.id}`).click();
|
||||
await expect(relationshipMultipleField).toContainText(relationTwoDoc.id);
|
||||
|
||||
await saveDocAndAssert(page);
|
||||
});
|
||||
|
||||
describe('existing relationships', () => {
|
||||
test('should highlight existing relationship', async () => {
|
||||
await page.goto(url.doc(docWithExistingRelations.id));
|
||||
|
||||
const fields = page.locator('.render-fields >> .react-select');
|
||||
const relationOneField = fields.nth(0);
|
||||
|
||||
// Check dropdown options
|
||||
await relationOneField.click({ delay: 100 });
|
||||
|
||||
await expect(page.locator('.rs__option--is-selected')).toHaveCount(1);
|
||||
await expect(page.locator('.rs__option--is-selected')).toHaveText(relationOneDoc.id);
|
||||
});
|
||||
|
||||
test('should show untitled ID on restricted relation', async () => {
|
||||
await page.goto(url.doc(docWithExistingRelations.id));
|
||||
|
||||
const fields = page.locator('.render-fields >> .react-select');
|
||||
const restrictedRelationField = fields.nth(3);
|
||||
|
||||
// Check existing relationship has untitled ID
|
||||
await expect(restrictedRelationField).toContainText(`Untitled - ID: ${restrictedRelation.id}`);
|
||||
|
||||
// Check dropdown options
|
||||
await restrictedRelationField.click({ delay: 100 });
|
||||
const options = page.locator('.rs__option');
|
||||
|
||||
await expect(options).toHaveCount(2); // None + 1 Unitled ID
|
||||
});
|
||||
|
||||
test('should show useAsTitle on relation', async () => {
|
||||
await page.goto(url.doc(docWithExistingRelations.id));
|
||||
|
||||
const fields = page.locator('.render-fields >> .react-select');
|
||||
const relationWithTitleField = fields.nth(4);
|
||||
|
||||
// Check existing relationship for correct title
|
||||
await expect(relationWithTitleField).toHaveText(relationWithTitle.name);
|
||||
|
||||
await relationWithTitleField.click({ delay: 100 });
|
||||
const options = page.locator('.rs__option');
|
||||
|
||||
await expect(options).toHaveCount(2); // None + 1 Doc
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function clearAllDocs(): Promise<void> {
|
||||
await clearCollectionDocs(slug);
|
||||
await clearCollectionDocs(relationOneSlug);
|
||||
await clearCollectionDocs(relationTwoSlug);
|
||||
await clearCollectionDocs(relationRestrictedSlug);
|
||||
await clearCollectionDocs(relationWithTitleSlug);
|
||||
}
|
||||
|
||||
async function clearCollectionDocs(collectionSlug: string): Promise<void> {
|
||||
const ids = (await payload.find({ collection: collectionSlug, limit: 100 })).docs.map((doc) => doc.id);
|
||||
await mapAsync(ids, async (id) => {
|
||||
await payload.delete({ collection: collectionSlug, id });
|
||||
});
|
||||
}
|
||||
49
test/e2e/helpers.ts
Normal file
49
test/e2e/helpers.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
import { expect } from '@playwright/test';
|
||||
import wait from '../../src/utilities/wait';
|
||||
|
||||
|
||||
export const credentials = {
|
||||
email: 'dev@payloadcms.com',
|
||||
password: 'test',
|
||||
roles: ['admin'],
|
||||
};
|
||||
|
||||
type FirstRegisterArgs = {
|
||||
page: Page,
|
||||
serverURL: string,
|
||||
}
|
||||
|
||||
type LoginArgs = {
|
||||
page: Page,
|
||||
serverURL: string,
|
||||
}
|
||||
|
||||
export async function firstRegister(args: FirstRegisterArgs): Promise<void> {
|
||||
const { page, serverURL } = args;
|
||||
|
||||
await page.goto(`${serverURL}/admin`);
|
||||
await page.fill('#email', credentials.email);
|
||||
await page.fill('#password', credentials.password);
|
||||
await page.fill('#confirm-password', credentials.password);
|
||||
await wait(500);
|
||||
await page.click('[type=submit]');
|
||||
await page.waitForURL(`${serverURL}/admin`);
|
||||
}
|
||||
|
||||
export async function login(args: LoginArgs): Promise<void> {
|
||||
const { page, serverURL } = args;
|
||||
|
||||
await page.goto(`${serverURL}/admin`);
|
||||
await page.fill('#email', credentials.email);
|
||||
await page.fill('#password', credentials.password);
|
||||
await wait(500);
|
||||
await page.click('[type=submit]');
|
||||
await page.waitForURL(`${serverURL}/admin`);
|
||||
}
|
||||
|
||||
export async function saveDocAndAssert(page: Page): Promise<void> {
|
||||
await page.click('text=Save', { delay: 100 });
|
||||
await expect(page.locator('.Toastify')).toContainText('successfully');
|
||||
expect(page.url()).not.toContain('create');
|
||||
}
|
||||
51
test/e2e/localization/config.ts
Normal file
51
test/e2e/localization/config.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { mapAsync } from '../../../src/utilities/mapAsync';
|
||||
import { buildConfig } from '../buildConfig';
|
||||
|
||||
export const slug = 'localized-posts';
|
||||
|
||||
export interface LocalizedPost {
|
||||
id: string
|
||||
title: string,
|
||||
description: string
|
||||
}
|
||||
|
||||
export default buildConfig({
|
||||
localization: {
|
||||
locales: [
|
||||
'en',
|
||||
'es',
|
||||
],
|
||||
defaultLocale: 'en',
|
||||
},
|
||||
collections: [{
|
||||
slug,
|
||||
access: {
|
||||
read: () => true,
|
||||
create: () => true,
|
||||
delete: () => true,
|
||||
update: () => true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
localized: true,
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
}],
|
||||
onInit: async (payload) => {
|
||||
await mapAsync([...Array(11)], async () => {
|
||||
await payload.create<LocalizedPost>({
|
||||
collection: slug,
|
||||
data: {
|
||||
title: 'title',
|
||||
description: 'description',
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
130
test/e2e/localization/index.spec.ts
Normal file
130
test/e2e/localization/index.spec.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
import payload from '../../../src';
|
||||
import type { TypeWithTimestamps } from '../../../src/collections/config/types';
|
||||
import { mapAsync } from '../../../src/utilities/mapAsync';
|
||||
import { AdminUrlUtil } from '../../helpers/adminUrlUtil';
|
||||
import { initPayloadTest } from '../../helpers/configHelpers';
|
||||
import { firstRegister, saveDocAndAssert } from '../helpers';
|
||||
import type { LocalizedPost } from './config';
|
||||
import { slug } from './config';
|
||||
|
||||
/**
|
||||
* TODO: Localization
|
||||
* - [x] create doc in spanish locale
|
||||
* - [x] retrieve doc in spanish locale
|
||||
* - [x] retrieve doc in default locale, check for null fields
|
||||
* - add translations in english to spanish doc
|
||||
* - [x] check locale toggle button
|
||||
*
|
||||
* Fieldtypes to test: (collections for each field type)
|
||||
* - localized and non-localized: array, block, group, relationship, text
|
||||
*
|
||||
* Repeat above for Globals
|
||||
*/
|
||||
|
||||
const { beforeAll, describe, afterEach } = test;
|
||||
let url: AdminUrlUtil;
|
||||
|
||||
const defaultLocale = 'en';
|
||||
const title = 'english title';
|
||||
const spanishTitle = 'spanish title';
|
||||
const description = 'description';
|
||||
|
||||
let page: Page;
|
||||
describe('Localization', () => {
|
||||
beforeAll(async ({ browser }) => {
|
||||
const { serverURL } = await initPayloadTest({
|
||||
__dirname,
|
||||
init: {
|
||||
local: false,
|
||||
},
|
||||
});
|
||||
|
||||
await clearDocs(); // Clear any seeded data from onInit
|
||||
|
||||
url = new AdminUrlUtil(serverURL, slug);
|
||||
|
||||
const context = await browser.newContext();
|
||||
page = await context.newPage();
|
||||
|
||||
await firstRegister({ page, serverURL });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await clearDocs();
|
||||
});
|
||||
|
||||
describe('localized text', () => {
|
||||
test('create english post, switch to spanish', async () => {
|
||||
await page.goto(url.create);
|
||||
|
||||
await fillValues({ title, description });
|
||||
await saveDocAndAssert(page);
|
||||
|
||||
// Change back to English
|
||||
await changeLocale('es');
|
||||
|
||||
// Localized field should not be populated
|
||||
await expect(page.locator('#title')).toBeEmpty();
|
||||
await expect(page.locator('#description')).toHaveValue(description);
|
||||
|
||||
await fillValues({ title: spanishTitle, description });
|
||||
await saveDocAndAssert(page);
|
||||
await changeLocale(defaultLocale);
|
||||
|
||||
// Expect english title
|
||||
await expect(page.locator('#title')).toHaveValue(title);
|
||||
await expect(page.locator('#description')).toHaveValue(description);
|
||||
});
|
||||
|
||||
test('create spanish post, add english', async () => {
|
||||
await page.goto(url.create);
|
||||
|
||||
const newLocale = 'es';
|
||||
|
||||
// Change to Spanish
|
||||
await changeLocale(newLocale);
|
||||
|
||||
await fillValues({ title: spanishTitle, description });
|
||||
await saveDocAndAssert(page);
|
||||
|
||||
// Change back to English
|
||||
await changeLocale(defaultLocale);
|
||||
|
||||
// Localized field should not be populated
|
||||
await expect(page.locator('#title')).toBeEmpty();
|
||||
await expect(page.locator('#description')).toHaveValue(description);
|
||||
|
||||
// Add English
|
||||
|
||||
await fillValues({ title, description });
|
||||
await saveDocAndAssert(page);
|
||||
await saveDocAndAssert(page);
|
||||
|
||||
await expect(page.locator('#title')).toHaveValue(title);
|
||||
await expect(page.locator('#description')).toHaveValue(description);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function clearDocs(): Promise<void> {
|
||||
const allDocs = await payload.find<LocalizedPost>({ collection: slug, limit: 100 });
|
||||
const ids = allDocs.docs.map((doc) => doc.id);
|
||||
await mapAsync(ids, async (id) => {
|
||||
await payload.delete({ collection: slug, id });
|
||||
});
|
||||
}
|
||||
|
||||
async function fillValues(data: Partial<Omit<LocalizedPost, keyof TypeWithTimestamps>>) {
|
||||
const { title: titleVal, description: descVal } = data;
|
||||
|
||||
if (titleVal) await page.locator('#title').fill(titleVal);
|
||||
if (descVal) await page.locator('#description').fill(descVal);
|
||||
}
|
||||
|
||||
async function changeLocale(newLocale: string) {
|
||||
await page.locator('.localizer >> button').first().click();
|
||||
await page.locator(`.localizer >> a:has-text("${newLocale}")`).click();
|
||||
expect(page.url()).toContain(`locale=${newLocale}`);
|
||||
}
|
||||
10
test/e2e/versions/config.ts
Normal file
10
test/e2e/versions/config.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { buildConfig } from '../buildConfig';
|
||||
|
||||
export const slug = 'slugname';
|
||||
|
||||
export default buildConfig({
|
||||
collections: [{
|
||||
slug,
|
||||
fields: [],
|
||||
}],
|
||||
});
|
||||
58
test/e2e/versions/index.template.ts
Normal file
58
test/e2e/versions/index.template.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { AdminUrlUtil } from '../../helpers/adminUrlUtil';
|
||||
import { initPayloadTest } from '../../helpers/configHelpers';
|
||||
import { firstRegister } from '../helpers';
|
||||
import { slug } from './config';
|
||||
|
||||
|
||||
/**
|
||||
* TODO: Versions, 3 separate collections
|
||||
* - drafts
|
||||
* - save draft before publishing
|
||||
* - publish immediately
|
||||
* - validation should be skipped when creating a draft
|
||||
*
|
||||
* - autosave
|
||||
* - versions (no drafts)
|
||||
* - version control shown
|
||||
* - assert version counts increment
|
||||
* - navigate to versions
|
||||
* - versions view accurately shows number of versions
|
||||
* - compare
|
||||
* - restore version
|
||||
* - specify locales to show
|
||||
*/
|
||||
|
||||
const { beforeAll, describe } = test;
|
||||
let url: AdminUrlUtil;
|
||||
|
||||
describe('suite name', () => {
|
||||
let page: Page;
|
||||
|
||||
beforeAll(async ({ browser }) => {
|
||||
const { serverURL } = await initPayloadTest({
|
||||
__dirname,
|
||||
init: {
|
||||
local: false,
|
||||
},
|
||||
});
|
||||
// await clearDocs(); // Clear any seeded data from onInit
|
||||
|
||||
url = new AdminUrlUtil(serverURL, slug);
|
||||
|
||||
const context = await browser.newContext();
|
||||
page = await context.newPage();
|
||||
|
||||
await firstRegister({ page, serverURL });
|
||||
});
|
||||
|
||||
// afterEach(async () => {
|
||||
// });
|
||||
|
||||
describe('feature', () => {
|
||||
test('testname', () => {
|
||||
expect(1).toEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
17
test/helpers/adminUrlUtil.ts
Normal file
17
test/helpers/adminUrlUtil.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export class AdminUrlUtil {
|
||||
admin: string;
|
||||
|
||||
collection: string;
|
||||
|
||||
create: string;
|
||||
|
||||
constructor(serverURL: string, slug: string) {
|
||||
this.admin = `${serverURL}/admin`;
|
||||
this.collection = `${this.admin}/collections/${slug}`;
|
||||
this.create = `${this.collection}/create`;
|
||||
}
|
||||
|
||||
doc(id: string): string {
|
||||
return `${this.collection}/${id}`;
|
||||
}
|
||||
}
|
||||
53
test/helpers/configHelpers.ts
Normal file
53
test/helpers/configHelpers.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import getPort from 'get-port';
|
||||
import path from 'path';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import express from 'express';
|
||||
import type { CollectionConfig } from '../../src/collections/config/types';
|
||||
import type { InitOptions } from '../../src/config/types';
|
||||
import payload from '../../src';
|
||||
|
||||
type Options = {
|
||||
__dirname: string
|
||||
init?: Partial<InitOptions>
|
||||
}
|
||||
|
||||
export async function initPayloadE2E(__dirname: string): Promise<{ serverURL: string }> {
|
||||
return initPayloadTest({
|
||||
__dirname,
|
||||
init: {
|
||||
local: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function initPayloadTest(options: Options): Promise<{ serverURL: string }> {
|
||||
const initOptions = {
|
||||
local: true,
|
||||
secret: uuid(),
|
||||
mongoURL: `mongodb://localhost/${uuid()}`,
|
||||
...options.init || {},
|
||||
};
|
||||
|
||||
process.env.PAYLOAD_CONFIG_PATH = path.resolve(options.__dirname, './config.ts');
|
||||
|
||||
const port = await getPort();
|
||||
|
||||
if (!initOptions?.local) {
|
||||
initOptions.express = express();
|
||||
}
|
||||
|
||||
await payload.init(initOptions);
|
||||
|
||||
if (initOptions.express) {
|
||||
initOptions.express.listen(port);
|
||||
}
|
||||
|
||||
return { serverURL: `http://localhost:${port}` };
|
||||
}
|
||||
|
||||
export const openAccess: CollectionConfig['access'] = {
|
||||
read: () => true,
|
||||
create: () => true,
|
||||
delete: () => true,
|
||||
update: () => true,
|
||||
};
|
||||
172
test/helpers/rest.ts
Normal file
172
test/helpers/rest.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import qs from 'qs';
|
||||
import type { Config } from '../../src/config/types';
|
||||
import type { PaginatedDocs } from '../../src/mongoose/types';
|
||||
import type { Where } from '../../src/types';
|
||||
|
||||
require('isomorphic-fetch');
|
||||
|
||||
type Args = {
|
||||
serverURL: string
|
||||
defaultSlug: string
|
||||
}
|
||||
|
||||
type LoginArgs = {
|
||||
email: string
|
||||
password: string
|
||||
collection: string
|
||||
}
|
||||
|
||||
type CreateArgs<T = any> = {
|
||||
slug?: string
|
||||
data: T
|
||||
auth?: boolean
|
||||
}
|
||||
|
||||
type FindArgs = {
|
||||
slug?: string
|
||||
query?: Where
|
||||
auth?: boolean
|
||||
}
|
||||
|
||||
type UpdateArgs<T = any> = {
|
||||
slug?: string
|
||||
id: string
|
||||
data: Partial<T>
|
||||
query?: any
|
||||
}
|
||||
|
||||
type DocResponse<T> = {
|
||||
status: number
|
||||
doc: T
|
||||
}
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: '',
|
||||
};
|
||||
|
||||
type QueryResponse<T> = {
|
||||
status: number;
|
||||
result: PaginatedDocs<T>;
|
||||
};
|
||||
|
||||
export class RESTClient {
|
||||
private readonly config: Config;
|
||||
|
||||
private token: string;
|
||||
|
||||
private serverURL: string;
|
||||
|
||||
private defaultSlug: string;
|
||||
|
||||
constructor(config: Config, args: Args) {
|
||||
this.config = config;
|
||||
this.serverURL = args.serverURL;
|
||||
this.defaultSlug = args.defaultSlug;
|
||||
}
|
||||
|
||||
async login(incomingArgs?: LoginArgs): Promise<string> {
|
||||
const args = incomingArgs ?? {
|
||||
email: 'dev@payloadcms.com',
|
||||
password: 'test',
|
||||
collection: 'users',
|
||||
};
|
||||
|
||||
const response = await fetch(`${this.serverURL}/api/${args.collection}/login`, {
|
||||
body: JSON.stringify({
|
||||
email: args.email,
|
||||
password: args.password,
|
||||
}),
|
||||
headers,
|
||||
method: 'post',
|
||||
});
|
||||
|
||||
const { token } = await response.json();
|
||||
|
||||
this.token = token;
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
async create<T = any>(args: CreateArgs): Promise<DocResponse<T>> {
|
||||
const options = {
|
||||
body: JSON.stringify(args.data),
|
||||
headers: {
|
||||
...headers,
|
||||
Authorization: '',
|
||||
},
|
||||
method: 'post',
|
||||
};
|
||||
|
||||
if (args.auth) {
|
||||
options.headers.Authorization = `JWT ${this.token}`;
|
||||
}
|
||||
|
||||
const slug = args.slug || this.defaultSlug;
|
||||
const response = await fetch(`${this.serverURL}/api/${slug}`, options);
|
||||
const { status } = response;
|
||||
const { doc } = await response.json();
|
||||
return { status, doc };
|
||||
}
|
||||
|
||||
async find<T = any>(args?: FindArgs): Promise<QueryResponse<T>> {
|
||||
const options = {
|
||||
headers: {
|
||||
...headers,
|
||||
Authorization: '',
|
||||
},
|
||||
};
|
||||
|
||||
if (args?.auth) {
|
||||
options.headers.Authorization = `JWT ${this.token}`;
|
||||
}
|
||||
|
||||
const slug = args?.slug || this.defaultSlug;
|
||||
const whereQuery = qs.stringify(args?.query ? { where: args.query } : {}, {
|
||||
addQueryPrefix: true,
|
||||
});
|
||||
const fetchURL = `${this.serverURL}/api/${slug}${whereQuery}`;
|
||||
const response = await fetch(fetchURL, options);
|
||||
const { status } = response;
|
||||
const result = await response.json();
|
||||
if (result.errors) throw new Error(result.errors[0].message);
|
||||
return { status, result };
|
||||
}
|
||||
|
||||
async update<T = any>(args: UpdateArgs<T>): Promise<DocResponse<T>> {
|
||||
const { slug, id, data, query } = args;
|
||||
const formattedQs = qs.stringify(query);
|
||||
const response = await fetch(
|
||||
`${this.serverURL}/api/${slug || this.defaultSlug}/${id}${formattedQs}`,
|
||||
{
|
||||
body: JSON.stringify(data),
|
||||
headers,
|
||||
method: 'put',
|
||||
},
|
||||
);
|
||||
const { status } = response;
|
||||
const json = await response.json();
|
||||
return { status, doc: json.doc };
|
||||
}
|
||||
|
||||
async findByID<T = any>(id: string, args?: { slug?: string }): Promise<DocResponse<T>> {
|
||||
const response = await fetch(`${this.serverURL}/api/${args?.slug || this.defaultSlug}/${id}`, {
|
||||
headers,
|
||||
method: 'get',
|
||||
});
|
||||
const { status } = response;
|
||||
const doc = await response.json();
|
||||
return { status, doc };
|
||||
}
|
||||
|
||||
async delete<T = any>(id: string, args?: { slug?: string }): Promise<DocResponse<T>> {
|
||||
const response = await fetch(`${this.serverURL}/api/${args?.slug || this.defaultSlug}/${id}`, {
|
||||
headers,
|
||||
method: 'delete',
|
||||
});
|
||||
const { status } = response;
|
||||
const doc = await response.json();
|
||||
return { status, doc };
|
||||
}
|
||||
}
|
||||
24
test/int/array-update/config.ts
Normal file
24
test/int/array-update/config.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { buildConfig } from '../buildConfig';
|
||||
|
||||
export default buildConfig({
|
||||
collections: [{
|
||||
slug: 'arrays',
|
||||
fields: [
|
||||
{
|
||||
name: 'array',
|
||||
type: 'array',
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'required',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'optional',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}],
|
||||
});
|
||||
108
test/int/array-update/index.spec.ts
Normal file
108
test/int/array-update/index.spec.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import mongoose from 'mongoose';
|
||||
import { initPayloadTest } from '../../helpers/configHelpers';
|
||||
import payload from '../../../src';
|
||||
import config from './config';
|
||||
|
||||
const collection = config.collections[0]?.slug;
|
||||
|
||||
describe('array-update', () => {
|
||||
beforeAll(async () => {
|
||||
await initPayloadTest({ __dirname });
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await mongoose.connection.dropDatabase();
|
||||
await mongoose.connection.close();
|
||||
await payload.mongoMemoryServer.stop();
|
||||
});
|
||||
|
||||
it('should persist existing array-based data while updating and passing row ID', async () => {
|
||||
const originalText = 'some optional text';
|
||||
|
||||
const doc = await payload.create({
|
||||
collection,
|
||||
data: {
|
||||
array: [
|
||||
{
|
||||
required: 'a required field here',
|
||||
optional: originalText,
|
||||
},
|
||||
{
|
||||
required: 'another required field here',
|
||||
optional: 'this is cool',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const arrayWithExistingValues = [
|
||||
...doc.array,
|
||||
];
|
||||
|
||||
const updatedText = 'this is some new text for the first item in array';
|
||||
|
||||
arrayWithExistingValues[0] = {
|
||||
id: arrayWithExistingValues[0].id,
|
||||
required: updatedText,
|
||||
};
|
||||
|
||||
|
||||
const updatedDoc = await payload.update({
|
||||
id: doc.id,
|
||||
collection,
|
||||
data: {
|
||||
array: arrayWithExistingValues,
|
||||
},
|
||||
});
|
||||
|
||||
expect(updatedDoc.array[0]).toMatchObject({
|
||||
required: updatedText,
|
||||
optional: originalText,
|
||||
});
|
||||
});
|
||||
|
||||
it('should disregard existing array-based data while updating and NOT passing row ID', async () => {
|
||||
const updatedText = 'here is some new text';
|
||||
|
||||
const secondArrayItem = {
|
||||
required: 'test',
|
||||
optional: 'optional test',
|
||||
};
|
||||
|
||||
const doc = await payload.create({
|
||||
collection,
|
||||
data: {
|
||||
array: [
|
||||
{
|
||||
required: 'a required field here',
|
||||
optional: 'some optional text',
|
||||
},
|
||||
secondArrayItem,
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const updatedDoc = await payload.update<any>({
|
||||
id: doc.id,
|
||||
collection,
|
||||
data: {
|
||||
array: [
|
||||
{
|
||||
required: updatedText,
|
||||
},
|
||||
{
|
||||
id: doc.array[1].id,
|
||||
required: doc.array[1].required,
|
||||
// NOTE - not passing optional field. It should persist
|
||||
// because we're passing ID
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(updatedDoc.array[0].required).toStrictEqual(updatedText);
|
||||
expect(updatedDoc.array[0].optional).toBeUndefined();
|
||||
|
||||
expect(updatedDoc.array[1]).toMatchObject(secondArrayItem);
|
||||
});
|
||||
});
|
||||
15
test/int/buildConfig.ts
Normal file
15
test/int/buildConfig.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import merge from 'deepmerge';
|
||||
import { buildConfig as buildPayloadConfig } from '../../src/config/build';
|
||||
import type { Config, SanitizedConfig } from '../../src/config/types';
|
||||
|
||||
export function buildConfig(overrides?: Partial<Config>): SanitizedConfig {
|
||||
const baseConfig: Config = {};
|
||||
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
baseConfig.admin = {
|
||||
disable: true,
|
||||
};
|
||||
}
|
||||
|
||||
return buildPayloadConfig(merge(baseConfig, overrides || {}));
|
||||
}
|
||||
15
test/int/collections-graphql/config.ts
Normal file
15
test/int/collections-graphql/config.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { openAccess } from '../../helpers/configHelpers';
|
||||
import { buildConfig } from '../buildConfig';
|
||||
|
||||
export default buildConfig({
|
||||
collections: [{
|
||||
slug: 'posts',
|
||||
access: openAccess,
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
}],
|
||||
});
|
||||
43
test/int/collections-graphql/index.spec.ts
Normal file
43
test/int/collections-graphql/index.spec.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import mongoose from 'mongoose';
|
||||
import { initPayloadTest } from '../../helpers/configHelpers';
|
||||
import config from './config';
|
||||
import payload from '../../../src';
|
||||
import { RESTClient } from '../../helpers/rest';
|
||||
|
||||
const collection = config.collections[0]?.slug;
|
||||
|
||||
let client: RESTClient;
|
||||
|
||||
describe('collections-graphql', () => {
|
||||
beforeAll(async () => {
|
||||
const { serverURL } = await initPayloadTest({ __dirname, init: { local: false } });
|
||||
client = new RESTClient(config, { serverURL });
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await mongoose.connection.dropDatabase();
|
||||
await mongoose.connection.close();
|
||||
await payload.mongoMemoryServer.stop();
|
||||
});
|
||||
|
||||
it('should create', async () => {
|
||||
const title = 'hello';
|
||||
|
||||
const { doc } = await client.create({
|
||||
slug: collection,
|
||||
data: {
|
||||
title,
|
||||
},
|
||||
});
|
||||
|
||||
expect(doc.title).toStrictEqual(title);
|
||||
});
|
||||
|
||||
it('should find', async () => {
|
||||
const { result } = await client.find({
|
||||
slug: collection,
|
||||
});
|
||||
|
||||
expect(result.totalDocs).toStrictEqual(1);
|
||||
});
|
||||
});
|
||||
105
test/int/collections-rest/config.ts
Normal file
105
test/int/collections-rest/config.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import type { CollectionConfig } from '../../../src/collections/config/types';
|
||||
import { buildConfig } from '../buildConfig';
|
||||
|
||||
export interface Post {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
number?: number;
|
||||
relationField?: Relation | string
|
||||
relationHasManyField?: RelationHasMany[] | string[]
|
||||
relationMultiRelationTo?: Relation[] | string[]
|
||||
}
|
||||
|
||||
export interface Relation {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export type RelationHasMany = Relation
|
||||
|
||||
const openAccess = {
|
||||
create: () => true,
|
||||
read: () => true,
|
||||
update: () => true,
|
||||
delete: () => true,
|
||||
};
|
||||
|
||||
const collectionWithName = (slug: string): CollectionConfig => {
|
||||
return {
|
||||
slug,
|
||||
access: openAccess,
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
export const slug = 'posts';
|
||||
export const relationSlug = 'relation-normal';
|
||||
export const relationHasManySlug = 'relation-has-many';
|
||||
export const relationMultipleRelationToSlug = 'relation-multi-relation-to';
|
||||
export default buildConfig({
|
||||
collections: [
|
||||
{
|
||||
slug,
|
||||
access: openAccess,
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'number',
|
||||
type: 'number',
|
||||
},
|
||||
// Relationship
|
||||
{
|
||||
name: 'relationField',
|
||||
type: 'relationship',
|
||||
relationTo: relationSlug,
|
||||
},
|
||||
// Relation hasMany
|
||||
{
|
||||
name: 'relationHasManyField',
|
||||
type: 'relationship',
|
||||
relationTo: relationHasManySlug,
|
||||
hasMany: true,
|
||||
},
|
||||
// Relation multiple relationTo
|
||||
{
|
||||
name: 'relationMultiRelationTo',
|
||||
type: 'relationship',
|
||||
relationTo: [relationSlug, 'dummy'],
|
||||
},
|
||||
],
|
||||
},
|
||||
collectionWithName(relationSlug),
|
||||
collectionWithName(relationHasManySlug),
|
||||
collectionWithName('dummy'),
|
||||
],
|
||||
onInit: async (payload) => {
|
||||
const rel1 = await payload.create<RelationHasMany>({
|
||||
collection: relationHasManySlug,
|
||||
data: {
|
||||
name: 'name',
|
||||
},
|
||||
});
|
||||
|
||||
await payload.create({
|
||||
collection: slug,
|
||||
data: {
|
||||
title: 'title',
|
||||
relationHasManyField: rel1.id,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
});
|
||||
433
test/int/collections-rest/index.spec.ts
Normal file
433
test/int/collections-rest/index.spec.ts
Normal file
@@ -0,0 +1,433 @@
|
||||
import mongoose from 'mongoose';
|
||||
import { initPayloadTest } from '../../helpers/configHelpers';
|
||||
import type { Relation, Post, RelationHasMany } from './config';
|
||||
import config, { relationHasManySlug, slug, relationSlug } from './config';
|
||||
import payload from '../../../src';
|
||||
import { RESTClient } from '../../helpers/rest';
|
||||
import { mapAsync } from '../../../src/utilities/mapAsync';
|
||||
|
||||
let client: RESTClient;
|
||||
|
||||
describe('collections-rest', () => {
|
||||
beforeAll(async () => {
|
||||
const { serverURL } = await initPayloadTest({ __dirname, init: { local: false } });
|
||||
client = new RESTClient(config, { serverURL, defaultSlug: slug });
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await mongoose.connection.dropDatabase();
|
||||
await mongoose.connection.close();
|
||||
await payload.mongoMemoryServer.stop();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await clearDocs();
|
||||
});
|
||||
|
||||
describe('CRUD', () => {
|
||||
it('should create', async () => {
|
||||
const data = {
|
||||
title: 'title',
|
||||
};
|
||||
const doc = await createPost(data);
|
||||
|
||||
expect(doc).toMatchObject(data);
|
||||
});
|
||||
|
||||
it('should find', async () => {
|
||||
const post1 = await createPost();
|
||||
const post2 = await createPost();
|
||||
const { status, result } = await client.find<Post>();
|
||||
|
||||
expect(status).toEqual(200);
|
||||
expect(result.totalDocs).toEqual(2);
|
||||
const expectedDocs = [post1, post2];
|
||||
expect(result.docs).toHaveLength(expectedDocs.length);
|
||||
expect(result.docs).toEqual(expect.arrayContaining(expectedDocs));
|
||||
});
|
||||
|
||||
it('should update existing', async () => {
|
||||
const { id, description } = await createPost({ description: 'desc' });
|
||||
const updatedTitle = 'updated-title';
|
||||
|
||||
const { status, doc: updated } = await client.update<Post>({
|
||||
id,
|
||||
data: { title: updatedTitle },
|
||||
});
|
||||
|
||||
expect(status).toEqual(200);
|
||||
expect(updated.title).toEqual(updatedTitle);
|
||||
expect(updated.description).toEqual(description); // Check was not modified
|
||||
});
|
||||
|
||||
it('should delete', async () => {
|
||||
const { id } = await createPost();
|
||||
|
||||
const { status, doc } = await client.delete<Post>(id);
|
||||
|
||||
expect(status).toEqual(200);
|
||||
expect(doc.id).toEqual(id);
|
||||
});
|
||||
|
||||
it('should include metadata', async () => {
|
||||
await createPosts(11);
|
||||
|
||||
const { result } = await client.find<Post>();
|
||||
|
||||
expect(result.totalDocs).toBeGreaterThan(0);
|
||||
expect(result.limit).toBe(10);
|
||||
expect(result.page).toBe(1);
|
||||
expect(result.pagingCounter).toBe(1);
|
||||
expect(result.hasPrevPage).toBe(false);
|
||||
expect(result.hasNextPage).toBe(true);
|
||||
expect(result.prevPage).toBeNull();
|
||||
expect(result.nextPage).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('Querying', () => {
|
||||
describe('Relationships', () => {
|
||||
it('should query nested relationship', async () => {
|
||||
const nameToQuery = 'name';
|
||||
const { doc: relation } = await client.create<Relation>({
|
||||
slug: relationSlug,
|
||||
data: {
|
||||
name: nameToQuery,
|
||||
},
|
||||
});
|
||||
|
||||
const post1 = await createPost({
|
||||
relationField: relation.id,
|
||||
});
|
||||
await createPost();
|
||||
|
||||
const { status, result } = await client.find<Post>({
|
||||
query: {
|
||||
'relationField.name': {
|
||||
equals: nameToQuery,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(status).toEqual(200);
|
||||
expect(result.docs).toEqual([post1]);
|
||||
expect(result.totalDocs).toEqual(1);
|
||||
});
|
||||
|
||||
it('should query nested relationship - hasMany', async () => {
|
||||
const nameToQuery = 'name';
|
||||
const { doc: relation } = await client.create<RelationHasMany>({
|
||||
slug: relationHasManySlug,
|
||||
data: {
|
||||
name: nameToQuery,
|
||||
},
|
||||
});
|
||||
|
||||
const post1 = await createPost({
|
||||
relationHasManyField: [relation.id],
|
||||
});
|
||||
await createPost();
|
||||
|
||||
const { status, result } = await client.find<Post>({
|
||||
query: {
|
||||
'relationHasManyField.name': {
|
||||
equals: nameToQuery,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(status).toEqual(200);
|
||||
expect(result.docs).toEqual([post1]);
|
||||
expect(result.totalDocs).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Operators', () => {
|
||||
it('equals', async () => {
|
||||
const valueToQuery = 'valueToQuery';
|
||||
const post1 = await createPost({ title: valueToQuery });
|
||||
await createPost();
|
||||
const { status, result } = await client.find<Post>({
|
||||
query: {
|
||||
title: {
|
||||
equals: valueToQuery,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(status).toEqual(200);
|
||||
expect(result.totalDocs).toEqual(1);
|
||||
expect(result.docs).toEqual([post1]);
|
||||
});
|
||||
|
||||
it('not_equals', async () => {
|
||||
const post1 = await createPost({ title: 'not-equals' });
|
||||
const post2 = await createPost();
|
||||
const { status, result } = await client.find<Post>({
|
||||
query: {
|
||||
title: {
|
||||
not_equals: post1.title,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(status).toEqual(200);
|
||||
expect(result.totalDocs).toEqual(1);
|
||||
expect(result.docs).toEqual([post2]);
|
||||
});
|
||||
|
||||
it('in', async () => {
|
||||
const post1 = await createPost({ title: 'my-title' });
|
||||
await createPost();
|
||||
const { status, result } = await client.find<Post>({
|
||||
query: {
|
||||
title: {
|
||||
in: [post1.title],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(status).toEqual(200);
|
||||
expect(result.docs).toEqual([post1]);
|
||||
expect(result.totalDocs).toEqual(1);
|
||||
});
|
||||
|
||||
it('not_in', async () => {
|
||||
const post1 = await createPost({ title: 'not-me' });
|
||||
const post2 = await createPost();
|
||||
const { status, result } = await client.find<Post>({
|
||||
query: {
|
||||
title: {
|
||||
not_in: [post1.title],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(status).toEqual(200);
|
||||
expect(result.docs).toEqual([post2]);
|
||||
expect(result.totalDocs).toEqual(1);
|
||||
});
|
||||
|
||||
it('like', async () => {
|
||||
const post1 = await createPost({ title: 'prefix-value' });
|
||||
await createPost();
|
||||
const { status, result } = await client.find<Post>({
|
||||
query: {
|
||||
title: {
|
||||
like: post1.title.substring(0, 6),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(status).toEqual(200);
|
||||
expect(result.docs).toEqual([post1]);
|
||||
expect(result.totalDocs).toEqual(1);
|
||||
});
|
||||
|
||||
it('exists - true', async () => {
|
||||
const postWithDesc = await createPost({ description: 'exists' });
|
||||
await createPost({ description: undefined });
|
||||
const { status, result } = await client.find<Post>({
|
||||
query: {
|
||||
description: {
|
||||
exists: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(status).toEqual(200);
|
||||
expect(result.totalDocs).toEqual(1);
|
||||
expect(result.docs).toEqual([postWithDesc]);
|
||||
});
|
||||
|
||||
it('exists - false', async () => {
|
||||
const postWithoutDesc = await createPost({ description: undefined });
|
||||
await createPost({ description: 'exists' });
|
||||
const { status, result } = await client.find<Post>({
|
||||
query: {
|
||||
description: {
|
||||
exists: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(status).toEqual(200);
|
||||
expect(result.totalDocs).toEqual(1);
|
||||
expect(result.docs).toEqual([postWithoutDesc]);
|
||||
});
|
||||
|
||||
describe('numbers', () => {
|
||||
let post1: Post;
|
||||
let post2: Post;
|
||||
beforeEach(async () => {
|
||||
post1 = await createPost({ number: 1 });
|
||||
post2 = await createPost({ number: 2 });
|
||||
});
|
||||
|
||||
it('greater_than', async () => {
|
||||
const { status, result } = await client.find<Post>({
|
||||
query: {
|
||||
number: {
|
||||
greater_than: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(status).toEqual(200);
|
||||
expect(result.totalDocs).toEqual(1);
|
||||
expect(result.docs).toEqual([post2]);
|
||||
});
|
||||
|
||||
it('greater_than_equal', async () => {
|
||||
const { status, result } = await client.find<Post>({
|
||||
query: {
|
||||
number: {
|
||||
greater_than_equal: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(status).toEqual(200);
|
||||
expect(result.totalDocs).toEqual(2);
|
||||
const expectedDocs = [post1, post2];
|
||||
expect(result.docs).toHaveLength(expectedDocs.length);
|
||||
expect(result.docs).toEqual(expect.arrayContaining(expectedDocs));
|
||||
});
|
||||
|
||||
it('less_than', async () => {
|
||||
const { status, result } = await client.find<Post>({
|
||||
query: {
|
||||
number: {
|
||||
less_than: 2,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(status).toEqual(200);
|
||||
expect(result.totalDocs).toEqual(1);
|
||||
expect(result.docs).toEqual([post1]);
|
||||
});
|
||||
|
||||
it('less_than_equal', async () => {
|
||||
const { status, result } = await client.find<Post>({
|
||||
query: {
|
||||
number: {
|
||||
less_than_equal: 2,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(status).toEqual(200);
|
||||
expect(result.totalDocs).toEqual(2);
|
||||
const expectedDocs = [post1, post2];
|
||||
expect(result.docs).toHaveLength(expectedDocs.length);
|
||||
expect(result.docs).toEqual(expect.arrayContaining(expectedDocs));
|
||||
});
|
||||
});
|
||||
|
||||
it('or', async () => {
|
||||
const post1 = await createPost({ title: 'post1' });
|
||||
const post2 = await createPost({ title: 'post2' });
|
||||
await createPost();
|
||||
|
||||
const { status, result } = await client.find<Post>({
|
||||
query: {
|
||||
or: [
|
||||
{
|
||||
title: {
|
||||
equals: post1.title,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: {
|
||||
equals: post2.title,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(status).toEqual(200);
|
||||
const expectedDocs = [post1, post2];
|
||||
expect(result.totalDocs).toEqual(expectedDocs.length);
|
||||
expect(result.docs).toEqual(expect.arrayContaining(expectedDocs));
|
||||
});
|
||||
|
||||
it('or - 1 result', async () => {
|
||||
const post1 = await createPost({ title: 'post1' });
|
||||
await createPost();
|
||||
|
||||
const { status, result } = await client.find<Post>({
|
||||
query: {
|
||||
or: [
|
||||
{
|
||||
title: {
|
||||
equals: post1.title,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: {
|
||||
equals: 'non-existent',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(status).toEqual(200);
|
||||
const expectedDocs = [post1];
|
||||
expect(result.totalDocs).toEqual(expectedDocs.length);
|
||||
expect(result.docs).toEqual(expect.arrayContaining(expectedDocs));
|
||||
});
|
||||
|
||||
it('and', async () => {
|
||||
const description = 'description';
|
||||
const post1 = await createPost({ title: 'post1', description });
|
||||
await createPost({ title: 'post2', description }); // Diff title, same desc
|
||||
await createPost();
|
||||
|
||||
const { status, result } = await client.find<Post>({
|
||||
query: {
|
||||
and: [
|
||||
{
|
||||
title: {
|
||||
equals: post1.title,
|
||||
},
|
||||
},
|
||||
{
|
||||
description: {
|
||||
equals: description,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(status).toEqual(200);
|
||||
expect(result.totalDocs).toEqual(1);
|
||||
expect(result.docs).toEqual([post1]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
async function createPost(overrides?: Partial<Post>) {
|
||||
const { doc } = await client.create<Post>({ data: { title: 'title', ...overrides } });
|
||||
return doc;
|
||||
}
|
||||
|
||||
async function createPosts(count: number) {
|
||||
await mapAsync([...Array(count)], async () => {
|
||||
await createPost();
|
||||
});
|
||||
}
|
||||
|
||||
async function clearDocs(): Promise<void> {
|
||||
const allDocs = await payload.find<Post>({ collection: slug, limit: 100 });
|
||||
const ids = allDocs.docs.map((doc) => doc.id);
|
||||
await mapAsync(ids, async (id) => {
|
||||
await payload.delete({ collection: slug, id });
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user