Co-authored-by: shikhantmaungs <shinkhantmaungs@gmail.com> Co-authored-by: Thomas Ghysels <info@thomasg.be> Co-authored-by: Kokutse Djoguenou <kokutse@Kokutses-MacBook-Pro.local> Co-authored-by: Christian Gil <47041342+ChrisGV04@users.noreply.github.com> Co-authored-by: Łukasz Rabiec <lukaszrabiec@gmail.com> Co-authored-by: Jenny <jennifer.eberlei@gmail.com> Co-authored-by: Hung Vu <hunghvu2017@gmail.com> Co-authored-by: Shin Khant Maung <101539335+shinkhantmaungs@users.noreply.github.com> Co-authored-by: Carlo Brualdi <carlo.brualdi@gmail.com> Co-authored-by: Ariel Tonglet <ariel.tonglet@gmail.com> Co-authored-by: Roman Ryzhikov <general+github@ya.ru> Co-authored-by: maekoya <maekoya@stromatolite.jp> Co-authored-by: Emilia Trollros <3m1l1a@emiliatrollros.se> Co-authored-by: Kokutse J Djoguenou <90865585+Julesdj@users.noreply.github.com> Co-authored-by: Mitch Dries <mitch.dries@gmail.com> BREAKING CHANGE: If you assigned labels to collections, globals or block names, you need to update your config! Your GraphQL schema and generated Typescript interfaces may have changed. Payload no longer uses labels for code based naming. To prevent breaking changes to your GraphQL API and typescript types in your project, you can assign the below properties to match what Payload previously generated for you from labels. On Collections Use `graphQL.singularName`, `graphQL.pluralName` for GraphQL schema names. Use `typescript.interface` for typescript generation name. On Globals Use `graphQL.name` for GraphQL Schema name. Use `typescript.interface` for typescript generation name. On Blocks (within Block fields) Use `graphQL.singularName` for graphQL schema names.
414 lines
14 KiB
TypeScript
414 lines
14 KiB
TypeScript
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 { login, saveDocAndAssert } from '../helpers';
|
|
import type { Post } from './config';
|
|
import { globalSlug, slug } from './shared';
|
|
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('admin', () => {
|
|
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 login({ 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-${slug}`);
|
|
await collectionLink.click();
|
|
|
|
expect(page.url()).toContain(url.list);
|
|
});
|
|
|
|
test('should nav to a global - sidebar', async () => {
|
|
await page.goto(url.admin);
|
|
await page.locator(`#nav-global-${globalSlug}`).click();
|
|
|
|
expect(page.url()).toContain(url.global(globalSlug));
|
|
});
|
|
|
|
test('should navigate to collection - card', async () => {
|
|
await page.goto(url.admin);
|
|
await page.locator(`#card-${slug}`).click();
|
|
expect(page.url()).toContain(url.list);
|
|
});
|
|
|
|
test('should collapse and expand collection groups', async () => {
|
|
await page.goto(url.admin);
|
|
const navGroup = page.locator('#nav-group-One .nav-group__toggle');
|
|
const link = await page.locator('#nav-group-one-collection-ones');
|
|
|
|
await expect(navGroup).toContainText('One');
|
|
await expect(link).toBeVisible();
|
|
|
|
await navGroup.click();
|
|
await expect(link).not.toBeVisible();
|
|
|
|
await navGroup.click();
|
|
await expect(link).toBeVisible();
|
|
});
|
|
|
|
test('should collapse and expand globals groups', async () => {
|
|
await page.goto(url.admin);
|
|
const navGroup = page.locator('#nav-group-Group .nav-group__toggle');
|
|
const link = await page.locator('#nav-global-group-globals-one');
|
|
|
|
await expect(navGroup).toContainText('Group');
|
|
await expect(link).toBeVisible();
|
|
|
|
await navGroup.click();
|
|
await expect(link).not.toBeVisible();
|
|
|
|
await navGroup.click();
|
|
await expect(link).toBeVisible();
|
|
});
|
|
|
|
test('should save nav group collapse preferences', async () => {
|
|
await page.goto(url.admin);
|
|
|
|
const navGroup = page.locator('#nav-group-One .nav-group__toggle');
|
|
await navGroup.click();
|
|
|
|
await page.goto(url.admin);
|
|
|
|
const link = await page.locator('#nav-group-one-collection-ones');
|
|
await expect(link).not.toBeVisible();
|
|
});
|
|
|
|
test('breadcrumbs - from list to dashboard', async () => {
|
|
await page.goto(url.list);
|
|
await page.locator('.step-nav a[href="/admin"]').click();
|
|
expect(page.url()).toContain(url.admin);
|
|
});
|
|
|
|
test('breadcrumbs - from document to collection', async () => {
|
|
const { id } = await createPost();
|
|
|
|
await page.goto(url.edit(id));
|
|
await page.locator(`.step-nav >> text=${slug}`).click();
|
|
expect(page.url()).toContain(url.list);
|
|
});
|
|
});
|
|
|
|
describe('CRUD', () => {
|
|
test('should create', async () => {
|
|
await page.goto(url.create);
|
|
await page.locator('#field-title').fill(title);
|
|
await page.locator('#field-description').fill(description);
|
|
await page.click('#action-save', { delay: 100 });
|
|
|
|
await saveDocAndAssert(page);
|
|
|
|
await expect(page.locator('#field-title')).toHaveValue(title);
|
|
await expect(page.locator('#field-description')).toHaveValue(description);
|
|
});
|
|
|
|
test('should read existing', async () => {
|
|
const { id } = await createPost();
|
|
|
|
await page.goto(url.edit(id));
|
|
|
|
await expect(page.locator('#field-title')).toHaveValue(title);
|
|
await expect(page.locator('#field-description')).toHaveValue(description);
|
|
});
|
|
|
|
test('should update existing', async () => {
|
|
const { id } = await createPost();
|
|
|
|
await page.goto(url.edit(id));
|
|
|
|
const newTitle = 'new title';
|
|
const newDesc = 'new description';
|
|
await page.locator('#field-title').fill(newTitle);
|
|
await page.locator('#field-description').fill(newDesc);
|
|
|
|
await saveDocAndAssert(page);
|
|
|
|
await expect(page.locator('#field-title')).toHaveValue(newTitle);
|
|
await expect(page.locator('#field-description')).toHaveValue(newDesc);
|
|
});
|
|
|
|
test('should delete existing', async () => {
|
|
const { id } = await createPost();
|
|
|
|
await page.goto(url.edit(id));
|
|
await page.locator('#action-delete').click();
|
|
await page.locator('#confirm-delete').click();
|
|
|
|
await expect(page.locator(`text=Post en "${id}" successfully deleted.`)).toBeVisible();
|
|
expect(page.url()).toContain(url.list);
|
|
});
|
|
|
|
test('should save globals', async () => {
|
|
await page.goto(url.global(globalSlug));
|
|
|
|
await page.locator('#field-title').fill(title);
|
|
await page.click('#action-save', { delay: 100 });
|
|
|
|
await expect(page.locator('.Toastify__toast--success')).toHaveCount(1);
|
|
await expect(page.locator('#field-title')).toHaveValue(title);
|
|
});
|
|
});
|
|
|
|
describe('i18n', () => {
|
|
test('should allow changing language', async () => {
|
|
await page.goto(url.account);
|
|
|
|
const field = page.locator('.account__language .react-select');
|
|
|
|
await field.click({ delay: 100 });
|
|
const options = page.locator('.rs__option');
|
|
await options.locator('text=Español').click();
|
|
|
|
await expect(page.locator('.step-nav')).toContainText('Tablero');
|
|
|
|
await field.click({ delay: 100 });
|
|
await options.locator('text=English').click();
|
|
await field.click({ delay: 100 });
|
|
await expect(page.locator('.form-submit .btn')).toContainText('Save');
|
|
});
|
|
|
|
test('should allow custom translation', async () => {
|
|
await expect(page.locator('.step-nav')).toContainText('Home');
|
|
});
|
|
});
|
|
|
|
describe('list view', () => {
|
|
const tableRowLocator = 'table >> tbody >> tr';
|
|
|
|
beforeEach(async () => {
|
|
await page.goto(url.list);
|
|
});
|
|
|
|
describe('filtering', () => {
|
|
test('search by id', async () => {
|
|
const { id } = await createPost();
|
|
await page.locator('.search-filter__input').fill(id);
|
|
await wait(250);
|
|
const tableItems = page.locator(tableRowLocator);
|
|
await expect(tableItems).toHaveCount(1);
|
|
});
|
|
|
|
test('search by title or description', async () => {
|
|
await createPost({
|
|
title: 'find me',
|
|
description: 'this is fun',
|
|
});
|
|
|
|
await page.locator('.search-filter__input').fill('find me');
|
|
await wait(250);
|
|
await expect(page.locator(tableRowLocator)).toHaveCount(1);
|
|
|
|
await page.locator('.search-filter__input').fill('this is fun');
|
|
await wait(250);
|
|
await expect(page.locator(tableRowLocator)).toHaveCount(1);
|
|
});
|
|
|
|
test('toggle columns', async () => {
|
|
const columnCountLocator = 'table >> thead >> tr >> th';
|
|
await createPost();
|
|
await page.locator('.list-controls__toggle-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('.list-controls__toggle-where').click();
|
|
await wait(1000); // Wait for column toggle UI, should probably use waitForSelector
|
|
|
|
await page.locator('.where-builder__add-first-filter').click();
|
|
|
|
const operatorField = page.locator('.condition__operator');
|
|
const valueField = page.locator('.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__actions-remove').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('custom css', () => {
|
|
test('should see custom css in admin UI', async () => {
|
|
await page.goto(url.admin);
|
|
const navControls = await page.locator('.nav__controls');
|
|
await expect(navControls).toHaveCSS('font-family', 'monospace');
|
|
});
|
|
});
|
|
|
|
// TODO: Troubleshoot flaky suite
|
|
describe.skip('sorting', () => {
|
|
beforeAll(async () => {
|
|
await createPost();
|
|
await createPost();
|
|
});
|
|
|
|
test('should sort', async () => {
|
|
const upChevron = page.locator('#heading-id .sort-column__asc');
|
|
const downChevron = page.locator('#heading-id .sort-column__desc');
|
|
|
|
const firstId = await page.locator('.row-1 .cell-id').innerText();
|
|
const secondId = await page.locator('.row-2 .cell-id').innerText();
|
|
|
|
await upChevron.click({ delay: 200 });
|
|
|
|
// Order should have swapped
|
|
expect(await page.locator('.row-1 .cell-id').innerText()).toEqual(secondId);
|
|
expect(await page.locator('.row-2 .cell-id').innerText()).toEqual(firstId);
|
|
|
|
await downChevron.click({ delay: 200 });
|
|
|
|
// Swap back
|
|
expect(await page.locator('.row-1 .cell-id').innerText()).toEqual(firstId);
|
|
expect(await page.locator('.row-2 .cell-id').innerText()).toEqual(secondId);
|
|
});
|
|
});
|
|
|
|
describe('i18n', () => {
|
|
test('should display translated collections and globals config options', async () => {
|
|
await page.goto(url.list);
|
|
|
|
// collection label
|
|
await expect(page.locator('#nav-posts')).toContainText('Posts en');
|
|
|
|
// global label
|
|
await expect(page.locator('#nav-global-global')).toContainText('Global en');
|
|
|
|
// view description
|
|
await expect(page.locator('.view-description')).toContainText('Description en');
|
|
});
|
|
|
|
test('should display translated field titles', async () => {
|
|
await createPost();
|
|
|
|
// column controls
|
|
await page.locator('.list-controls__toggle-columns').click();
|
|
await expect(await page.locator('.column-selector__column >> text=Title en')).toHaveText('Title en');
|
|
|
|
// filters
|
|
await page.locator('.list-controls__toggle-where').click();
|
|
await page.locator('.where-builder__add-first-filter').click();
|
|
await page.locator('.condition__field .rs__control').click();
|
|
const options = page.locator('.rs__option');
|
|
await expect(await options.locator('text=Title en')).toHaveText('Title en');
|
|
|
|
// list columns
|
|
await expect(await page.locator('#heading-title .sort-column__label')).toHaveText('Title en');
|
|
await expect(await page.locator('.search-filter input')).toHaveAttribute('placeholder', /(Title en)/);
|
|
});
|
|
|
|
test('should use fallback language on field titles', async () => {
|
|
// change language German
|
|
await page.goto(url.account);
|
|
const field = page.locator('.account__language .react-select');
|
|
await field.click({ delay: 100 });
|
|
const languageSelect = page.locator('.rs__option');
|
|
// text field does not have a 'de' label
|
|
await languageSelect.locator('text=Deutsch').click();
|
|
|
|
await page.goto(url.list);
|
|
await page.locator('.list-controls__toggle-columns').click();
|
|
// expecting the label to fall back to english as default fallbackLng
|
|
await expect(await page.locator('.column-selector__column >> text=Title en')).toHaveText('Title en');
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
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 });
|
|
});
|
|
}
|