add-cms
This commit is contained in:
9
source/admin/cypress/utils/README.md
Normal file
9
source/admin/cypress/utils/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
## Utilities for integration tests
|
||||
|
||||
Utils in this dir must be explicitly included in each spec file, e.g.:
|
||||
|
||||
```
|
||||
import '../utils/dismiss-local-backup';
|
||||
```
|
||||
|
||||
For routines to be executed on all tests, please use the `cypress/plugins.index.js` file instead: https://docs.cypress.io/guides/core-concepts/writing-and-organizing-tests.html#Plugin-files
|
||||
42
source/admin/cypress/utils/config.js
Normal file
42
source/admin/cypress/utils/config.js
Normal file
@@ -0,0 +1,42 @@
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const yaml = require('js-yaml');
|
||||
|
||||
const devTestDirectory = path.join(__dirname, '..', '..', 'dev-test');
|
||||
const backendsDirectory = path.join(devTestDirectory, 'backends');
|
||||
|
||||
async function copyBackendFiles(backend) {
|
||||
await Promise.all(
|
||||
['config.yml', 'index.html'].map(file => {
|
||||
return fs.copyFile(
|
||||
path.join(backendsDirectory, backend, file),
|
||||
path.join(devTestDirectory, file),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async function updateConfig(configModifier) {
|
||||
const configFile = path.join(devTestDirectory, 'config.yml');
|
||||
const configContent = await fs.readFile(configFile);
|
||||
const config = yaml.load(configContent);
|
||||
await configModifier(config);
|
||||
await fs.writeFileSync(configFile, yaml.dump(config));
|
||||
}
|
||||
|
||||
async function switchVersion(version) {
|
||||
const htmlFile = path.join(devTestDirectory, 'index.html');
|
||||
const content = await fs.readFile(htmlFile);
|
||||
|
||||
const replaceString =
|
||||
version === 'latest'
|
||||
? '<script src="dist/decap-cms.js"></script>'
|
||||
: `<script src="https://unpkg.com/decap-cms@${version}/dist/decap-cms.js"></script>`;
|
||||
|
||||
await fs.writeFile(
|
||||
htmlFile,
|
||||
content.toString().replace(/<script src=".+?decap-cms.+?"><\/script>/, replaceString),
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = { copyBackendFiles, updateConfig, switchVersion };
|
||||
51
source/admin/cypress/utils/constants.js
Normal file
51
source/admin/cypress/utils/constants.js
Normal file
@@ -0,0 +1,51 @@
|
||||
const workflowStatus = { draft: 'Drafts', review: 'In Review', ready: 'Ready' };
|
||||
const editorStatus = { draft: 'Draft', review: 'In review', ready: 'Ready' };
|
||||
const setting1 = { limit: 10, author: 'John Doe' };
|
||||
const setting2 = { name: 'Jane Doe', description: 'description' };
|
||||
const publishTypes = { publishNow: 'Publish now', publishAndCreateNew: 'Publish and create new', publishAndDuplicate: 'Publish and duplicate' };
|
||||
const colorError = 'rgb(255, 0, 59)';
|
||||
const colorNormal = 'rgb(223, 223, 227)';
|
||||
const textColorNormal = 'rgb(68, 74, 87)';
|
||||
|
||||
const notifications = {
|
||||
saved: 'Entry saved',
|
||||
published: 'Entry published',
|
||||
unpublished: 'Entry unpublished',
|
||||
updated: 'Entry status updated',
|
||||
deletedUnpublished: 'Unpublished changes deleted',
|
||||
error: {
|
||||
missingField: "Oops, you've missed a required field. Please complete before saving.",
|
||||
},
|
||||
validation: {
|
||||
range: {
|
||||
fieldLabel: 'Number of posts on frontpage',
|
||||
message: 'Number of posts on frontpage must be between 1 and 10.',
|
||||
},
|
||||
},
|
||||
};
|
||||
const HOT_KEY_MAP = {
|
||||
'bold': 'mod+b',
|
||||
'code': 'mod+shift+c',
|
||||
'italic': 'mod+i',
|
||||
'strikethrough': 'mod+shift+s',
|
||||
'heading-one': 'mod+1',
|
||||
'heading-two': 'mod+2',
|
||||
'heading-three': 'mod+3',
|
||||
'heading-four': 'mod+4',
|
||||
'heading-five': 'mod+5',
|
||||
'heading-six': 'mod+6',
|
||||
'link': 'mod+k',
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
workflowStatus,
|
||||
editorStatus,
|
||||
setting1,
|
||||
setting2,
|
||||
notifications,
|
||||
publishTypes,
|
||||
HOT_KEY_MAP,
|
||||
colorError,
|
||||
colorNormal,
|
||||
textColorNormal,
|
||||
};
|
||||
17
source/admin/cypress/utils/dismiss-local-backup.js
Normal file
17
source/admin/cypress/utils/dismiss-local-backup.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { en } from '../../packages/decap-cms-locales/src';
|
||||
|
||||
// Prevents unsaved changes in dev local storage from being used
|
||||
Cypress.on('window:confirm', message => {
|
||||
const {
|
||||
editor: {
|
||||
editor: { confirmLoadBackup },
|
||||
},
|
||||
} = en;
|
||||
|
||||
switch (message) {
|
||||
case confirmLoadBackup:
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
});
|
||||
69
source/admin/cypress/utils/mock-server.js
Normal file
69
source/admin/cypress/utils/mock-server.js
Normal file
@@ -0,0 +1,69 @@
|
||||
const { mockServerClient } = require('mockserver-client');
|
||||
const mockserver = require('mockserver-node');
|
||||
|
||||
const PROXY_PORT = 1080;
|
||||
const PROXY_HOST = 'localhost';
|
||||
|
||||
const start = () =>
|
||||
mockserver.start_mockserver({
|
||||
serverPort: PROXY_PORT,
|
||||
});
|
||||
|
||||
const stop = () =>
|
||||
mockserver.stop_mockserver({
|
||||
serverPort: PROXY_PORT,
|
||||
});
|
||||
|
||||
const retrieveRecordedExpectations = async () => {
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
mockServerClient(PROXY_HOST, PROXY_PORT)
|
||||
.retrieveRecordedExpectations({})
|
||||
.then(resolve, reject);
|
||||
});
|
||||
|
||||
let timeout;
|
||||
const timeoutPromise = new Promise(resolve => {
|
||||
timeout = setTimeout(() => {
|
||||
console.warn('retrieveRecordedExpectations timeout');
|
||||
resolve([]);
|
||||
}, 3000);
|
||||
});
|
||||
|
||||
let recorded = await Promise.race([promise, timeoutPromise]);
|
||||
clearTimeout(timeout);
|
||||
|
||||
recorded = recorded.filter(({ httpRequest }) => {
|
||||
const { Host = [] } = httpRequest.headers;
|
||||
|
||||
// Host is an array of strings
|
||||
return (
|
||||
Host.includes('api.github.com') ||
|
||||
(Host.includes('gitlab.com') && httpRequest.path.includes('api/v4')) ||
|
||||
Host.includes('api.bitbucket.org') ||
|
||||
(Host.includes('bitbucket.org') && httpRequest.path.includes('info/lfs')) ||
|
||||
Host.includes('api.media.atlassian.com') ||
|
||||
Host.some(host => host.includes('netlify.com')) ||
|
||||
Host.some(host => host.includes('netlify.app')) ||
|
||||
Host.some(host => host.includes('s3.amazonaws.com'))
|
||||
);
|
||||
});
|
||||
|
||||
return recorded;
|
||||
};
|
||||
|
||||
const resetMockServerState = async () => {
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
mockServerClient(PROXY_HOST, PROXY_PORT)
|
||||
.reset()
|
||||
.then(resolve, reject);
|
||||
});
|
||||
|
||||
await promise;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
start,
|
||||
stop,
|
||||
resetMockServerState,
|
||||
retrieveRecordedExpectations,
|
||||
};
|
||||
5
source/admin/cypress/utils/regexp.js
Normal file
5
source/admin/cypress/utils/regexp.js
Normal file
@@ -0,0 +1,5 @@
|
||||
const escapeRegExp = string => {
|
||||
return string.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||
};
|
||||
|
||||
module.exports = { escapeRegExp };
|
||||
744
source/admin/cypress/utils/steps.js
Normal file
744
source/admin/cypress/utils/steps.js
Normal file
@@ -0,0 +1,744 @@
|
||||
const {
|
||||
notifications,
|
||||
workflowStatus,
|
||||
editorStatus,
|
||||
publishTypes,
|
||||
colorError,
|
||||
colorNormal,
|
||||
textColorNormal,
|
||||
} = require('./constants');
|
||||
|
||||
function login(user) {
|
||||
cy.viewport(1200, 1200);
|
||||
if (user) {
|
||||
cy.visit('/', {
|
||||
onBeforeLoad: () => {
|
||||
// https://github.com/cypress-io/cypress/issues/1208
|
||||
window.indexedDB.deleteDatabase('localforage');
|
||||
window.localStorage.setItem('decap-cms-user', JSON.stringify(user));
|
||||
if (user.netlifySiteURL) {
|
||||
window.localStorage.setItem('netlifySiteURL', user.netlifySiteURL);
|
||||
}
|
||||
},
|
||||
});
|
||||
if (user.netlifySiteURL && user.email && user.password) {
|
||||
cy.get('input[name="email"]')
|
||||
.clear()
|
||||
.type(user.email);
|
||||
cy.get('input[name="password"]')
|
||||
.clear()
|
||||
.type(user.password);
|
||||
cy.contains('button', 'Login').click();
|
||||
}
|
||||
} else {
|
||||
cy.visit('/');
|
||||
cy.contains('button', 'Login').click();
|
||||
}
|
||||
cy.contains('a', 'New Post');
|
||||
}
|
||||
|
||||
function assertNotification(message) {
|
||||
// if clock is use, a tick is needed for toastify to show the notifications
|
||||
cy.clock().then(clock => {
|
||||
if (clock) {
|
||||
advanceClock(clock);
|
||||
}
|
||||
cy.get('.notif__container').within(() => {
|
||||
cy.contains(message);
|
||||
cy.contains(message).invoke('hide');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function assertColorOn(cssProperty, color, opts) {
|
||||
if (opts.type && opts.type === 'label') {
|
||||
(opts.scope ? opts.scope : cy).contains('label', opts.label).should($el => {
|
||||
expect($el).to.have.css(cssProperty, color);
|
||||
});
|
||||
} else if (opts.type && opts.type === 'field') {
|
||||
const assertion = $el => expect($el).to.have.css(cssProperty, color);
|
||||
if (opts.isMarkdown) {
|
||||
(opts.scope ? opts.scope : cy)
|
||||
.contains('label', opts.label)
|
||||
.next()
|
||||
.children()
|
||||
.eq(0)
|
||||
.children()
|
||||
.eq(1)
|
||||
.should(assertion);
|
||||
} else {
|
||||
(opts.scope ? opts.scope : cy)
|
||||
.contains('label', opts.label)
|
||||
.parents()
|
||||
.next()
|
||||
.should(assertion);
|
||||
}
|
||||
} else if (opts.el) {
|
||||
opts.el.should($el => {
|
||||
expect($el).to.have.css(cssProperty, color);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function exitEditor() {
|
||||
cy.contains('a', 'Writing in').click();
|
||||
}
|
||||
|
||||
function goToWorkflow() {
|
||||
cy.contains('a', 'Workflow').click();
|
||||
}
|
||||
|
||||
function goToCollections() {
|
||||
cy.contains('a', 'Content').click();
|
||||
}
|
||||
|
||||
function goToMediaLibrary() {
|
||||
cy.contains('button', 'Media').click();
|
||||
}
|
||||
|
||||
function assertUnpublishedEntryInEditor() {
|
||||
cy.contains('button', 'Delete unpublished entry');
|
||||
}
|
||||
|
||||
function assertPublishedEntryInEditor() {
|
||||
cy.contains('button', 'Delete published entry');
|
||||
}
|
||||
|
||||
function assertUnpublishedChangesInEditor() {
|
||||
cy.contains('button', 'Delete unpublished changes');
|
||||
}
|
||||
|
||||
function goToEntry(entry) {
|
||||
goToCollections();
|
||||
cy.get('a h2')
|
||||
.first()
|
||||
.contains(entry.title)
|
||||
.click();
|
||||
}
|
||||
|
||||
function updateWorkflowStatus({ title }, fromColumnHeading, toColumnHeading) {
|
||||
cy.contains('h2', fromColumnHeading)
|
||||
.parent()
|
||||
.contains('a', title)
|
||||
.drag();
|
||||
cy.contains('h2', toColumnHeading)
|
||||
.parent()
|
||||
.drop();
|
||||
cy.clock().then(clock => {
|
||||
if (clock) {
|
||||
advanceClock(clock);
|
||||
}
|
||||
assertNotification(notifications.updated);
|
||||
});
|
||||
}
|
||||
|
||||
function publishWorkflowEntry({ title }, timeout) {
|
||||
cy.contains('h2', workflowStatus.ready, { timeout })
|
||||
.parent()
|
||||
.within(() => {
|
||||
cy.contains('a', title)
|
||||
.parent()
|
||||
.within(() => {
|
||||
cy.contains('button', 'Publish new entry').click({ force: true });
|
||||
});
|
||||
});
|
||||
// assertNotification(notifications.published);
|
||||
}
|
||||
|
||||
function deleteWorkflowEntry({ title }) {
|
||||
cy.contains('a', title)
|
||||
.parent()
|
||||
.within(() => {
|
||||
cy.contains('button', 'Delete new entry').click({ force: true });
|
||||
});
|
||||
|
||||
assertNotification(notifications.deletedUnpublished);
|
||||
}
|
||||
|
||||
const STATUS_BUTTON_TEXT = 'Status:';
|
||||
|
||||
function assertWorkflowStatusInEditor(status) {
|
||||
cy.contains('[role="button"]', STATUS_BUTTON_TEXT).as('setStatusButton');
|
||||
cy.get('@setStatusButton')
|
||||
.parent()
|
||||
.within(() => {
|
||||
cy.get('@setStatusButton').click();
|
||||
cy.contains('[role="menuitem"] span', status)
|
||||
.parent()
|
||||
.within(() => {
|
||||
cy.get('svg');
|
||||
});
|
||||
cy.get('@setStatusButton').click();
|
||||
});
|
||||
}
|
||||
|
||||
function assertPublishedEntry(entry) {
|
||||
if (Array.isArray(entry)) {
|
||||
const entries = entry.reverse();
|
||||
cy.get('a h2').then(els => {
|
||||
cy.wrap(els.slice(0, entries.length)).each((el, idx) => {
|
||||
cy.wrap(el).contains(entries[idx].title);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
cy.get('a h2')
|
||||
.first()
|
||||
.contains(entry.title);
|
||||
}
|
||||
}
|
||||
|
||||
function deleteEntryInEditor() {
|
||||
cy.contains('button', 'Delete').click();
|
||||
assertNotification(notifications.deletedUnpublished);
|
||||
}
|
||||
|
||||
function assertOnCollectionsPage() {
|
||||
cy.url().should('contain', '/#/collections/posts');
|
||||
}
|
||||
|
||||
function assertEntryDeleted(entry) {
|
||||
cy.get('body').then($body => {
|
||||
const entriesHeaders = $body.find('a h2');
|
||||
if (entriesHeaders.length > 0) {
|
||||
if (Array.isArray(entry)) {
|
||||
const titles = entry.map(e => e.title);
|
||||
cy.get('a h2').each(el => {
|
||||
expect(titles).not.to.include(el.text());
|
||||
});
|
||||
} else {
|
||||
cy.get('a h2').each(el => {
|
||||
expect(entry.title).not.to.equal(el.text());
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function assertWorkflowStatus({ title }, status) {
|
||||
cy.contains('h2', status)
|
||||
.parent()
|
||||
.contains('a', title);
|
||||
}
|
||||
|
||||
function updateWorkflowStatusInEditor(newStatus) {
|
||||
selectDropdownItem(STATUS_BUTTON_TEXT, newStatus);
|
||||
assertNotification(notifications.updated);
|
||||
}
|
||||
|
||||
function publishEntryInEditor(publishType) {
|
||||
selectDropdownItem('Publish', publishType);
|
||||
assertNotification(notifications.published);
|
||||
}
|
||||
|
||||
function publishAndCreateNewEntryInEditor() {
|
||||
selectDropdownItem('Publish', publishTypes.publishAndCreateNew);
|
||||
assertNotification(notifications.published);
|
||||
cy.url().should('eq', `http://localhost:8080/#/collections/posts/new`);
|
||||
cy.get('[id^="title-field"]').should('have.value', '');
|
||||
}
|
||||
|
||||
function publishAndDuplicateEntryInEditor(entry) {
|
||||
selectDropdownItem('Publish', publishTypes.publishAndDuplicate);
|
||||
assertNotification(notifications.published);
|
||||
cy.url().should('eq', `http://localhost:8080/#/collections/posts/new`);
|
||||
cy.get('[id^="title-field"]').should('have.value', entry.title);
|
||||
}
|
||||
|
||||
function selectDropdownItem(label, item) {
|
||||
cy.contains('[role="button"]', label).as('dropDownButton');
|
||||
cy.get('@dropDownButton')
|
||||
.parent()
|
||||
.within(() => {
|
||||
cy.get('@dropDownButton').click();
|
||||
cy.contains('[role="menuitem"] span', item).click();
|
||||
});
|
||||
}
|
||||
|
||||
function flushClockAndSave() {
|
||||
cy.clock().then(clock => {
|
||||
// some input fields are de-bounced thus require advancing the clock
|
||||
if (clock) {
|
||||
advanceClock(clock);
|
||||
}
|
||||
|
||||
cy.contains('button', 'Save').click();
|
||||
});
|
||||
}
|
||||
|
||||
function populateEntry(entry, onDone = flushClockAndSave) {
|
||||
const keys = Object.keys(entry);
|
||||
for (const key of keys) {
|
||||
const value = entry[key];
|
||||
if (key === 'body') {
|
||||
cy.getMarkdownEditor()
|
||||
.first()
|
||||
.click()
|
||||
.clear({ force: true })
|
||||
.type(value, { force: true });
|
||||
} else {
|
||||
cy.get(`[id^="${key}-field"]`)
|
||||
.first()
|
||||
.clear({ force: true });
|
||||
cy.get(`[id^="${key}-field"]`)
|
||||
.first()
|
||||
.type(value, { force: true });
|
||||
}
|
||||
}
|
||||
|
||||
onDone();
|
||||
}
|
||||
|
||||
function newPost() {
|
||||
// click even if covered by toast
|
||||
cy.contains('a', 'New Post').click({ force: true });
|
||||
}
|
||||
|
||||
function createPost(entry) {
|
||||
newPost();
|
||||
populateEntry(entry);
|
||||
}
|
||||
|
||||
function createPostAndExit(entry) {
|
||||
createPost(entry);
|
||||
cy.clock().then(clock => { advanceClock(clock); });
|
||||
exitEditor();
|
||||
}
|
||||
|
||||
function advanceClock(clock) {
|
||||
if (clock) {
|
||||
// https://github.com/cypress-io/cypress/issues/1273
|
||||
clock.tick(150);
|
||||
clock.tick(150);
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(500);
|
||||
}
|
||||
}
|
||||
|
||||
function publishEntry({ createNew = false, duplicate = false } = {}) {
|
||||
cy.clock().then(clock => {
|
||||
// some input fields are de-bounced thus require advancing the clock
|
||||
advanceClock(clock);
|
||||
|
||||
if (createNew) {
|
||||
advanceClock(clock);
|
||||
selectDropdownItem('Publish', publishTypes.publishAndCreateNew);
|
||||
advanceClock(clock);
|
||||
} else if (duplicate) {
|
||||
advanceClock(clock);
|
||||
selectDropdownItem('Publish', publishTypes.publishAndDuplicate);
|
||||
advanceClock(clock);
|
||||
} else {
|
||||
selectDropdownItem('Publish', publishTypes.publishNow);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(500);
|
||||
});
|
||||
}
|
||||
|
||||
function createPostAndPublish(entry) {
|
||||
newPost();
|
||||
populateEntry(entry, publishEntry);
|
||||
exitEditor();
|
||||
}
|
||||
|
||||
function createPostPublishAndCreateNew(entry) {
|
||||
newPost();
|
||||
populateEntry(entry, () => publishEntry({ createNew: true }));
|
||||
cy.url().should('eq', `http://localhost:8080/#/collections/posts/new`);
|
||||
// TODO: fix this test
|
||||
// previous entry data is somehow not cleared from the editor when opening new post
|
||||
// cy.get('[id^="title-field"]').should('have.value', '');
|
||||
|
||||
exitEditor();
|
||||
}
|
||||
|
||||
function createPostPublishAndDuplicate(entry) {
|
||||
newPost();
|
||||
populateEntry(entry, () => publishEntry({ duplicate: true }));
|
||||
cy.url().should('eq', `http://localhost:8080/#/collections/posts/new`);
|
||||
cy.get('[id^="title-field"]').should('have.value', entry.title);
|
||||
|
||||
exitEditor();
|
||||
}
|
||||
|
||||
function editPostAndPublish(entry1, entry2) {
|
||||
goToEntry(entry1);
|
||||
cy.contains('button', 'Delete entry');
|
||||
cy.contains('span', 'Published');
|
||||
|
||||
populateEntry(entry2, publishEntry);
|
||||
// existing entry slug should remain the same after save
|
||||
cy.url().should(
|
||||
'eq',
|
||||
`http://localhost:8080/#/collections/posts/entries/1970-01-01-${entry1.title
|
||||
.toLowerCase()
|
||||
.replace(/\s/, '-')}`,
|
||||
);
|
||||
}
|
||||
|
||||
function editPostPublishAndCreateNew(entry1, entry2) {
|
||||
goToEntry(entry1);
|
||||
cy.contains('button', 'Delete entry');
|
||||
cy.contains('span', 'Published');
|
||||
|
||||
populateEntry(entry2, () => publishEntry({ createNew: true }));
|
||||
cy.url().should('eq', `http://localhost:8080/#/collections/posts/new`);
|
||||
cy.get('[id^="title-field"]').should('have.value', '');
|
||||
}
|
||||
|
||||
function editPostPublishAndDuplicate(entry1, entry2) {
|
||||
goToEntry(entry1);
|
||||
cy.contains('button', 'Delete entry');
|
||||
cy.contains('span', 'Published');
|
||||
|
||||
populateEntry(entry2, () => publishEntry({ duplicate: true }));
|
||||
cy.url().should('eq', `http://localhost:8080/#/collections/posts/new`);
|
||||
cy.get('[id^="title-field"]').should('have.value', entry2.title);
|
||||
}
|
||||
|
||||
function duplicatePostAndPublish(entry1) {
|
||||
goToEntry(entry1);
|
||||
cy.contains('button', 'Delete entry');
|
||||
selectDropdownItem('Published', 'Duplicate');
|
||||
publishEntry();
|
||||
|
||||
cy.url().should(
|
||||
'eq',
|
||||
`http://localhost:8080/#/collections/posts/entries/1970-01-01-${entry1.title
|
||||
.toLowerCase()
|
||||
.replace(/\s/, '-')}-1`,
|
||||
);
|
||||
}
|
||||
|
||||
function updateExistingPostAndExit(fromEntry, toEntry) {
|
||||
goToWorkflow();
|
||||
cy.contains('h2', fromEntry.title)
|
||||
.parent()
|
||||
.click({ force: true });
|
||||
populateEntry(toEntry);
|
||||
exitEditor();
|
||||
goToWorkflow();
|
||||
cy.contains('h2', toEntry.title);
|
||||
}
|
||||
|
||||
function unpublishEntry(entry) {
|
||||
goToCollections();
|
||||
cy.contains('h2', entry.title)
|
||||
.parent()
|
||||
.click({ force: true });
|
||||
selectDropdownItem('Published', 'Unpublish');
|
||||
assertNotification(notifications.unpublished);
|
||||
goToWorkflow();
|
||||
assertWorkflowStatus(entry, workflowStatus.ready);
|
||||
}
|
||||
|
||||
function duplicateEntry(entry) {
|
||||
selectDropdownItem('Published', 'Duplicate');
|
||||
cy.url().should('contain', '/#/collections/posts/new');
|
||||
flushClockAndSave();
|
||||
updateWorkflowStatusInEditor(editorStatus.ready);
|
||||
publishEntryInEditor(publishTypes.publishNow);
|
||||
exitEditor();
|
||||
cy.get('a h2').should($h2s => {
|
||||
expect($h2s.eq(0)).to.contain(entry.title);
|
||||
expect($h2s.eq(1)).to.contain(entry.title);
|
||||
});
|
||||
}
|
||||
|
||||
function validateObjectFields({ limit, author }) {
|
||||
cy.get('a[href^="#/collections/settings"]').click();
|
||||
cy.get('a[href^="#/collections/settings/entries/general"]').click();
|
||||
cy.get('input[type=number]').type(limit);
|
||||
cy.contains('button', 'Save').click();
|
||||
assertNotification(notifications.error.missingField);
|
||||
assertFieldErrorStatus('Default Author', colorError);
|
||||
cy.contains('label', 'Default Author').click();
|
||||
cy.focused().type(author);
|
||||
cy.contains('button', 'Save').click();
|
||||
assertNotification(notifications.saved);
|
||||
assertFieldErrorStatus('Default Author', colorNormal);
|
||||
}
|
||||
|
||||
function validateNestedObjectFields({ limit, author }) {
|
||||
cy.get('a[href^="#/collections/settings"]').click();
|
||||
cy.get('a[href^="#/collections/settings/entries/general"]').click();
|
||||
cy.contains('label', 'Default Author').click();
|
||||
cy.focused().type(author);
|
||||
cy.contains('button', 'Save').click();
|
||||
assertNotification(notifications.error.missingField);
|
||||
cy.get('input[type=number]').type(limit + 1);
|
||||
cy.contains('button', 'Save').click();
|
||||
assertFieldValidationError(notifications.validation.range);
|
||||
cy.get('input[type=number]')
|
||||
.clear()
|
||||
.type(-1);
|
||||
cy.contains('button', 'Save').click();
|
||||
assertFieldValidationError(notifications.validation.range);
|
||||
cy.get('input[type=number]')
|
||||
.clear()
|
||||
.type(limit);
|
||||
cy.contains('button', 'Save').click();
|
||||
assertNotification(notifications.saved);
|
||||
}
|
||||
|
||||
function validateListFields({ name, description }) {
|
||||
cy.get('a[href^="#/collections/settings"]').click();
|
||||
cy.get('a[href^="#/collections/settings/entries/authors"]').click();
|
||||
cy.contains('button', 'Add').click();
|
||||
cy.contains('button', 'Save').click();
|
||||
assertNotification(notifications.error.missingField);
|
||||
assertFieldErrorStatus('Authors', colorError);
|
||||
cy.get('div[class*=SortableListItem]')
|
||||
.eq(2)
|
||||
.as('listControl');
|
||||
assertFieldErrorStatus('Name', colorError, { scope: cy.get('@listControl') });
|
||||
assertColorOn('background-color', colorError, {
|
||||
type: 'label',
|
||||
label: 'Description',
|
||||
scope: cy.get('@listControl'),
|
||||
isMarkdown: true,
|
||||
});
|
||||
assertListControlErrorStatus([colorError, colorError], '@listControl');
|
||||
cy.get('input')
|
||||
.eq(2)
|
||||
.type(name);
|
||||
cy.getMarkdownEditor()
|
||||
.eq(2)
|
||||
.type(description);
|
||||
flushClockAndSave();
|
||||
assertNotification(notifications.saved);
|
||||
assertFieldErrorStatus('Authors', colorNormal);
|
||||
}
|
||||
|
||||
function validateNestedListFields() {
|
||||
cy.get('a[href^="#/collections/settings"]').click();
|
||||
cy.get('a[href^="#/collections/settings/entries/hotel_locations"]').click();
|
||||
|
||||
// add first city list item
|
||||
cy.contains('button', 'hotel locations').click();
|
||||
cy.contains('button', 'cities').click();
|
||||
cy.contains('label', 'City')
|
||||
.parents()
|
||||
.next()
|
||||
.first()
|
||||
.type('Washington DC');
|
||||
cy.contains('label', 'Number of Hotels in City')
|
||||
.parents()
|
||||
.next()
|
||||
.first()
|
||||
.type('5');
|
||||
cy.contains('button', 'city locations').click();
|
||||
|
||||
// add second city list item
|
||||
cy.contains('button', 'cities').click();
|
||||
cy.contains('label', 'Cities')
|
||||
.parents()
|
||||
.next()
|
||||
.first()
|
||||
.find('div[class*=SortableListItem]')
|
||||
.eq(2)
|
||||
.as('secondCitiesListControl');
|
||||
cy.get('@secondCitiesListControl')
|
||||
.contains('label', 'City')
|
||||
.parents()
|
||||
.next()
|
||||
.first()
|
||||
.type('Boston');
|
||||
cy.get('@secondCitiesListControl')
|
||||
.contains('button', 'city locations')
|
||||
.click();
|
||||
|
||||
cy.contains('button', 'Save').click();
|
||||
assertNotification(notifications.error.missingField);
|
||||
|
||||
// assert on fields
|
||||
assertFieldErrorStatus('Hotel Locations', colorError);
|
||||
assertFieldErrorStatus('Cities', colorError);
|
||||
assertFieldErrorStatus('City', colorNormal);
|
||||
assertFieldErrorStatus('City', colorNormal, { scope: cy.get('@secondCitiesListControl') });
|
||||
assertFieldErrorStatus('Number of Hotels in City', colorNormal);
|
||||
assertFieldErrorStatus('Number of Hotels in City', colorError, {
|
||||
scope: cy.get('@secondCitiesListControl'),
|
||||
});
|
||||
assertFieldErrorStatus('City Locations', colorError);
|
||||
assertFieldErrorStatus('City Locations', colorError, {
|
||||
scope: cy.get('@secondCitiesListControl'),
|
||||
});
|
||||
assertFieldErrorStatus('Hotel Name', colorError);
|
||||
assertFieldErrorStatus('Hotel Name', colorError, { scope: cy.get('@secondCitiesListControl') });
|
||||
|
||||
// list control aliases
|
||||
cy.contains('label', 'Hotel Locations')
|
||||
.parents()
|
||||
.next()
|
||||
.find('div[class*=SortableListItem]')
|
||||
.first()
|
||||
.as('hotelLocationsListControl');
|
||||
cy.contains('label', 'Cities')
|
||||
.parents()
|
||||
.next()
|
||||
.find('div[class*=SortableListItem]')
|
||||
.eq(0)
|
||||
.as('firstCitiesListControl');
|
||||
cy.contains('label', 'City Locations')
|
||||
.parents()
|
||||
.next()
|
||||
.find('div[class*=SortableListItem]')
|
||||
.eq(0)
|
||||
.as('firstCityLocationsListControl');
|
||||
cy.contains('label', 'Cities')
|
||||
.parents()
|
||||
.next()
|
||||
.find('div[class*=SortableListItem]')
|
||||
.eq(3)
|
||||
.as('secondCityLocationsListControl');
|
||||
|
||||
// assert on list controls
|
||||
assertListControlErrorStatus([colorError, colorError], '@hotelLocationsListControl');
|
||||
assertListControlErrorStatus([colorError, colorError], '@firstCitiesListControl');
|
||||
assertListControlErrorStatus([colorError, colorError], '@secondCitiesListControl');
|
||||
assertListControlErrorStatus([colorError, colorError], '@firstCityLocationsListControl');
|
||||
assertListControlErrorStatus([colorError, colorError], '@secondCityLocationsListControl');
|
||||
|
||||
cy.contains('label', 'Hotel Name')
|
||||
.parents()
|
||||
.next()
|
||||
.first()
|
||||
.type('The Ritz Carlton');
|
||||
cy.contains('button', 'Save').click();
|
||||
assertNotification(notifications.error.missingField);
|
||||
assertListControlErrorStatus([colorNormal, textColorNormal], '@firstCitiesListControl');
|
||||
|
||||
// fill out rest of form and save
|
||||
cy.get('@secondCitiesListControl')
|
||||
.contains('label', 'Number of Hotels in City')
|
||||
.parents()
|
||||
.next()
|
||||
.first()
|
||||
.type(3);
|
||||
cy.get('@secondCitiesListControl')
|
||||
.contains('label', 'Hotel Name')
|
||||
.parents()
|
||||
.next()
|
||||
.first()
|
||||
.type('Grand Hyatt');
|
||||
cy.contains('label', 'Country')
|
||||
.parents()
|
||||
.next()
|
||||
.first()
|
||||
.type('United States');
|
||||
flushClockAndSave();
|
||||
assertNotification(notifications.saved);
|
||||
}
|
||||
|
||||
function validateObjectFieldsAndExit(setting) {
|
||||
validateObjectFields(setting);
|
||||
exitEditor();
|
||||
}
|
||||
|
||||
function validateNestedObjectFieldsAndExit(setting) {
|
||||
validateNestedObjectFields(setting);
|
||||
exitEditor();
|
||||
}
|
||||
|
||||
function validateListFieldsAndExit(setting) {
|
||||
validateListFields(setting);
|
||||
exitEditor();
|
||||
}
|
||||
|
||||
function validateNestedListFieldsAndExit(setting) {
|
||||
validateNestedListFields(setting);
|
||||
exitEditor();
|
||||
}
|
||||
|
||||
function assertFieldValidationError({ message, fieldLabel }) {
|
||||
cy.contains('label', fieldLabel)
|
||||
.siblings('ul[class*=ControlErrorsList]')
|
||||
.contains(message);
|
||||
assertFieldErrorStatus(fieldLabel, colorError);
|
||||
}
|
||||
|
||||
function assertFieldErrorStatus(label, color, opts = { isMarkdown: false }) {
|
||||
assertColorOn('background-color', color, {
|
||||
type: 'label',
|
||||
label,
|
||||
scope: opts.scope,
|
||||
isMarkdown: opts.isMarkdown,
|
||||
});
|
||||
assertColorOn('border-color', color, {
|
||||
type: 'field',
|
||||
label,
|
||||
scope: opts.scope,
|
||||
isMarkdown: opts.isMarkdown,
|
||||
});
|
||||
}
|
||||
|
||||
function assertListControlErrorStatus(colors = ['', ''], alias) {
|
||||
cy.get(alias).within(() => {
|
||||
// assert list item border has correct color
|
||||
assertColorOn('border-right-color', colors[0], {
|
||||
el: cy
|
||||
.root()
|
||||
.children()
|
||||
.eq(2),
|
||||
});
|
||||
// collapse list item
|
||||
cy.get('button[class*=TopBarButton-button]')
|
||||
.first()
|
||||
.click();
|
||||
// assert list item label text has correct color
|
||||
assertColorOn('color', colors[1], { el: cy.get('div[class*=NestedObjectLabel]').first() });
|
||||
// uncollapse list item
|
||||
cy.get('button[class*=TopBarButton-button]')
|
||||
.first()
|
||||
.click();
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
login,
|
||||
createPost,
|
||||
createPostAndExit,
|
||||
createPostAndPublish,
|
||||
updateExistingPostAndExit,
|
||||
exitEditor,
|
||||
goToWorkflow,
|
||||
goToCollections,
|
||||
goToMediaLibrary,
|
||||
updateWorkflowStatus,
|
||||
publishWorkflowEntry,
|
||||
deleteWorkflowEntry,
|
||||
assertWorkflowStatusInEditor,
|
||||
assertPublishedEntry,
|
||||
deleteEntryInEditor,
|
||||
assertOnCollectionsPage,
|
||||
assertEntryDeleted,
|
||||
assertWorkflowStatus,
|
||||
updateWorkflowStatusInEditor,
|
||||
validateObjectFieldsAndExit,
|
||||
validateNestedObjectFieldsAndExit,
|
||||
validateListFieldsAndExit,
|
||||
validateNestedListFieldsAndExit,
|
||||
unpublishEntry,
|
||||
publishEntryInEditor,
|
||||
duplicateEntry,
|
||||
newPost,
|
||||
populateEntry,
|
||||
goToEntry,
|
||||
publishEntry,
|
||||
createPostPublishAndCreateNew,
|
||||
createPostPublishAndDuplicate,
|
||||
editPostAndPublish,
|
||||
editPostPublishAndCreateNew,
|
||||
editPostPublishAndDuplicate,
|
||||
duplicatePostAndPublish,
|
||||
publishAndCreateNewEntryInEditor,
|
||||
publishAndDuplicateEntryInEditor,
|
||||
assertNotification,
|
||||
assertFieldValidationError,
|
||||
flushClockAndSave,
|
||||
assertPublishedEntryInEditor,
|
||||
assertUnpublishedEntryInEditor,
|
||||
assertUnpublishedChangesInEditor,
|
||||
};
|
||||
Reference in New Issue
Block a user