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('netlify-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) { cy.get('.notif__container').within(() => { cy.contains(message); // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(500); 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) .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(); 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); } function assertWorkflowStatusInEditor(status) { cy.contains('[role="button"]', 'Set status').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('Set status', 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) { // 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); } cy.contains('button', 'Save').click(); assertNotification(notifications.saved); }); } 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() { cy.contains('a', 'New Post').click(); } function createPost(entry) { newPost(); populateEntry(entry); } function createPostAndExit(entry) { createPost(entry); 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`); 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*=ListControl]') .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') .next() .type('Washington DC'); cy.contains('label', 'Number of Hotels in City') .next() .type('5'); cy.contains('button', 'city locations').click(); // add second city list item cy.contains('button', 'cities').click(); cy.contains('label', 'Cities') .next() .find('div[class*=ListControl]') .eq(2) .as('secondCitiesListControl'); cy.get('@secondCitiesListControl') .contains('label', 'City') .next() .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') .next() .find('div[class*=ListControl]') .first() .as('hotelLocationsListControl'); cy.contains('label', 'Cities') .next() .find('div[class*=ListControl]') .eq(0) .as('firstCitiesListControl'); cy.contains('label', 'City Locations') .next() .find('div[class*=ListControl]') .eq(0) .as('firstCityLocationsListControl'); cy.contains('label', 'Cities') .next() .find('div[class*=ListControl]') .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') .next() .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') .type(3); cy.get('@secondCitiesListControl') .contains('label', 'Hotel Name') .type('Grand Hyatt'); cy.contains('label', 'Country') .next() .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, };