import 'cypress-real-events'; import format from 'date-fns/format'; import { editorStatus, notifications, publishTypes, workflowStatus } from './constants'; import { WorkflowStatus } from '@staticcms/core/constants/publishModes'; import type { Author, Post, User } from '../interface'; export interface LoginProps { user?: User; editorialWorkflow?: boolean; } export function login(options?: LoginProps) { const { user, editorialWorkflow = false } = options ?? {}; cy.viewport(1200, 1200); if (user) { cy.visit('/', { onBeforeLoad: () => { // https://github.com/cypress-io/cypress/issues/1208 window.indexedDB.deleteDatabase('localforage'); window.localStorage.setItem('static-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(); } if (editorialWorkflow) { cy.contains('div', 'Editorial Workflow'); } else { cy.contains('a', 'New Post'); } } export function assertNotification(message: string) { cy.get('[data-testid="toast-messages"]', { timeout: 10000 }) .should('be.visible') .within(() => { cy.contains(message); cy.contains(message).invoke('hide'); }); } export function exitEditor() { cy.get('[data-testid="breadcrumb-link"]').first().click(); } export function goToWorkflow() { cy.get('[data-testid="sidebar-nav-Dashboard"]').click(); } export function goToMediaLibrary() { cy.get('[data-testid="sidebar-nav-Media]').click(); } export function assertUnpublishedEntryInEditor() { cy.contains('button', 'Delete unpublished entry'); } export function assertPublishedEntryInEditor() { cy.contains('button', 'Delete published entry'); } export function assertUnpublishedChangesInEditor() { cy.contains('button', 'Delete unpublished changes'); } export function goToEntry(entry: Post) { cy.contains('a', entry.Title).click(); } export function updateWorkflowStatus( { Title }: Post, fromColumnHeading: WorkflowStatus, toColumnHeading: WorkflowStatus, ) { cy.get(`[data-testid="drop-zone-${fromColumnHeading}"]`).within(() => { cy.get(`[data-testid="drag-handle-${Title}"]`).dragTo( `[data-testid="drop-zone-${toColumnHeading}"]`, ); }); assertNotification(notifications.updated); } export function publishWorkflowEntry({ Title }: Post, timeout = 3000) { cy.get(`[data-testid="drop-zone-${WorkflowStatus.PENDING_PUBLISH}"]`).within(() => { cy.get(`[data-testid="drag-handle-${Title}"]`, { timeout }) .realHover() .within(() => { cy.get('[data-testid="workflow-dashboard-publish"]').click(); }); }); cy.get('[data-testid="confirm-button"]').click(); assertNotification(notifications.published); } export function deleteWorkflowEntry({ Title }: Post) { cy.contains('a', Title) .parent() .within(() => { cy.contains('button', 'Delete new entry').click({ force: true }); }); assertNotification(notifications.deletedUnpublished); } const STATUS_BUTTON_TEXT = 'Status:'; export function assertWorkflowStatusInEditor(status: string) { cy.contains('button', STATUS_BUTTON_TEXT).as('setStatusButton'); cy.get('@setStatusButton').click(); cy.contains('[role="menuitem"] div', status) .parent() .within(() => { cy.get('svg'); }); cy.get('@setStatusButton').click(); } export function assertPublishedEntry(entry: Post | Post[]) { if (Array.isArray(entry)) { const entries = entry.reverse(); cy.wrap(entry.slice(0, entries.length)).each((_el, idx) => { cy.contains('a', entries[idx].Title); }); } else { cy.contains('a', entry.Title); } } export function deleteEntryInEditor() { cy.contains('button', 'Delete').click(); assertNotification(notifications.deletedUnpublished); } export function assertOnCollectionsPage() { cy.url().should('contain', '/#/collections/posts'); } export function assertEntryDeleted(entry: Post) { cy.get('body').then($body => { const entriesHeaders = $body.find('a'); if (entriesHeaders.length > 0) { if (Array.isArray(entry)) { const titles = entry.map(e => e.title); cy.get('a').each(el => { expect(titles).not.to.include(el.text()); }); } else { cy.get('a').each(el => { expect(entry.Title).not.to.equal(el.text()); }); } } }); } export function assertWorkflowStatus({ Title }: Post, status: string) { cy.contains('h2', status).parent().contains('a', Title); } export function updateWorkflowStatusInEditor(newStatus: string) { selectDropdownItem(STATUS_BUTTON_TEXT, newStatus); assertNotification(notifications.updated); } export function publishEntryInEditor(publishType: string) { selectDropdownItem('Publish', publishType); assertNotification(notifications.published); } export function publishAndCreateNewEntryInEditor() { selectDropdownItem('Publish', publishTypes.publishAndCreateNew); assertNotification(notifications.published); cy.url().should('eq', `http://localhost:8080/#/collections/posts/new`); cy.get('[data-testid="field-Title"]').should('have.value', ''); } export function publishAndDuplicateEntryInEditor(entry: Post) { selectDropdownItem('Publish', publishTypes.publishAndDuplicate); assertNotification(notifications.published); cy.url().should('eq', `http://localhost:8080/#/collections/posts/new`); cy.get('[data-testid="field-Title"]').should('have.value', entry.Title); } function selectDropdownItem(label: string, item: string) { cy.contains('button', label).click(); cy.contains('[role="menuitem"] div', item).click(); } function flushClockAndSave() { cy.wait(260); cy.contains('button', 'Save').should('not.be.disabled').click(); assertNotification(notifications.saved); } export function populateEntry(entry: Post, onDone = flushClockAndSave) { const keys = Object.keys(entry) as (keyof Post)[]; for (const key of keys) { const value = entry[key]; if (key === 'Body') { cy.getMarkdownEditor().click().clear({ force: true }).type(value, { force: true }); cy.getMarkdownEditor().first().click().clear({ force: true }).type(value, { force: true }); } else { cy.get(`[data-testid="field-${key}"]`).click(); cy.focused().clear({ force: true }); cy.focused().type(value, { force: true }); } } onDone(); } function newPost() { cy.contains('a', 'New Post').click(); } export function createPost(entry: Post) { newPost(); populateEntry(entry); } export function createPostAndExit(entry: Post) { createPost(entry); exitEditor(); } function publishEntry({ createNew = false, duplicate = false } = {}) { cy.wait(500); if (createNew) { selectDropdownItem('Publish', publishTypes.publishAndCreateNew); } else if (duplicate) { selectDropdownItem('Publish', publishTypes.publishAndDuplicate); } else { selectDropdownItem('Publish', publishTypes.publishNow); } assertNotification(notifications.saved); // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(500); } export function createPostAndPublish(entry: Post) { newPost(); populateEntry(entry, publishEntry); exitEditor(); } export function createPostPublishAndCreateNew(entry: Post) { newPost(); populateEntry(entry, () => publishEntry({ createNew: true })); cy.url().should('eq', `http://localhost:8080/#/collections/posts/new`); cy.get('[data-testid="field-Title"] input').should('have.value', ''); exitEditor(); } export function createPostPublishAndDuplicate(entry: Post) { newPost(); populateEntry(entry, () => publishEntry({ duplicate: true })); cy.url().should('eq', `http://localhost:8080/#/collections/posts/new?duplicate=true`); cy.get('[data-testid="field-Title"] input').should('have.value', entry.Title); exitEditor(); } export function editPostAndPublish(entry1: Post, entry2: Post) { goToEntry(entry1); cy.wait(1000); cy.get('[data-testid="editor-extra-menu"]', { timeout: 5000 }).should('be.enabled'); cy.get('[data-testid="editor-extra-menu"]').click(); cy.get('[data-testid="delete-button"]'); cy.contains('[data-testid="publish-dropdown"]', 'Published'); populateEntry(entry2, publishEntry); // existing entry slug should remain the same after save cy.url().should( 'eq', `http://localhost:8080/#/collections/posts/entries/${format( new Date(), 'yyyy-MM-dd', )}-${entry1.Title.toLowerCase().replace(/\s/, '-')}`, ); } export function editPostPublishAndCreateNew(entry1: Post, entry2: Post) { goToEntry(entry1); cy.wait(1000); cy.get('[data-testid="editor-extra-menu"]', { timeout: 5000 }).should('be.enabled'); cy.get('[data-testid="editor-extra-menu"]').click(); cy.get('[data-testid="delete-button"]'); cy.contains('[data-testid="publish-dropdown"]', 'Published'); populateEntry(entry2, () => publishEntry({ createNew: true })); cy.url().should('eq', `http://localhost:8080/#/collections/posts/new`); cy.get('[data-testid="field-Title"] input').should('have.value', ''); } export function editPostPublishAndDuplicate(entry1: Post, entry2: Post) { goToEntry(entry1); cy.wait(1000); cy.get('[data-testid="editor-extra-menu"]', { timeout: 5000 }).should('be.enabled'); cy.get('[data-testid="editor-extra-menu"]').click(); cy.get('[data-testid="delete-button"]'); cy.contains('[data-testid="publish-dropdown"]', 'Published'); populateEntry(entry2, () => publishEntry({ duplicate: true })); cy.url().should('eq', `http://localhost:8080/#/collections/posts/new?duplicate=true`); cy.get('[data-testid="field-Title"] input').should('have.value', entry2.Title); } export function duplicatePostAndPublish(entry1: Post) { goToEntry(entry1); cy.wait(1000); cy.get('[data-testid="editor-extra-menu"]', { timeout: 5000 }).should('be.enabled'); cy.get('[data-testid="editor-extra-menu"]').click(); cy.get('[data-testid="delete-button"]'); selectDropdownItem('Published', 'Duplicate'); publishEntry(); cy.url().should( 'eq', `http://localhost:8080/#/collections/posts/entries/${format( new Date(), 'yyyy-MM-dd', )}-${entry1.Title.toLowerCase().replace(/\s/, '-')}-1`, ); } export function updateExistingPostAndExit(fromEntry: Post, toEntry: Post) { goToWorkflow(); cy.contains('a', fromEntry.Title).click({ force: true }); populateEntry(toEntry); exitEditor(); goToWorkflow(); cy.contains('a', toEntry.Title); } export function unpublishEntry(entry: Post) { cy.contains('a', entry.Title).click({ force: true }); selectDropdownItem('Published', 'Unpublish'); assertNotification(notifications.unpublished); goToWorkflow(); assertWorkflowStatus(entry, workflowStatus.ready); } export function duplicateEntry(entry: Post) { selectDropdownItem('Published', 'Duplicate'); cy.url().should('contain', '/#/collections/posts/new?duplicate=true'); flushClockAndSave(); updateWorkflowStatusInEditor(editorStatus.ready); publishEntryInEditor(publishTypes.publishNow); exitEditor(); cy.get('a').should($h2s => { expect($h2s.eq(0)).to.contain(entry.Title); expect($h2s.eq(1)).to.contain(entry.Title); }); } export interface ValidateObjectFieldsProps { limit: string; author: string; } function validateObjectFields({ limit, author }: ValidateObjectFieldsProps) { cy.contains('a', 'Settings').click(); cy.contains('a', 'Site Settings').click(); discardDraft(); cy.contains('label', 'Number of posts on frontpage').click(); cy.focused().type(limit); flushClockAndSave(); assertNotification(notifications.error.missingField); cy.get('[data-testid="field-Default Author"]').should('have.class', 'CMS_Field_error'); cy.contains('label', 'Default Author').click(); cy.focused().type(author); flushClockAndSave(); assertNotification(notifications.saved); cy.get('[data-testid="field-Default Author"]').should('not.have.class', 'CMS_Field_error'); } function validateNestedObjectFields({ limit, author }: ValidateObjectFieldsProps) { cy.contains('a', 'Settings').click(); cy.contains('a', 'Site Settings').click(); discardDraft(); cy.contains('label', 'Default Author').click(); cy.focused().type(author); flushClockAndSave(); assertNotification(notifications.error.missingField); cy.get('input[type=number]').type(limit + 1); flushClockAndSave(); assertFieldValidationError(notifications.validation.range); cy.get('input[type=number]').clear().type('-1'); flushClockAndSave(); assertFieldValidationError(notifications.validation.range); cy.get('input[type=number]').clear().type(limit); flushClockAndSave(); assertNotification(notifications.saved); } function validateListFields({ name, description }: Author) { cy.contains('a', 'Settings').click(); cy.contains('a', 'Authors').click(); discardDraft(); cy.contains('button', 'Add').click(); flushClockAndSave(); assertNotification(notifications.error.missingField); cy.get('[data-testid="list-field-Authors"]').should('have.class', 'CMS_WidgetList_error'); cy.get('[data-testid="list-item-field-Author"]').eq(2).as('listControl'); cy.get('@listControl').should('have.class', 'CMS_WidgetList_ListItem_error'); cy.get('@listControl').get('[data-testid="field-Name"]').should('have.class', 'CMS_Field_error'); cy.get('input').eq(2).type(name); cy.get('textarea').eq(2).type(description); flushClockAndSave(); assertNotification(notifications.saved); cy.get('[data-testid="list-field-Authors"]').should('not.have.class', 'CMS_WidgetList_error'); } function validateNestedListFields() { cy.contains('a', 'Settings').click(); cy.contains('a', 'Hotel Locations').click(); discardDraft(); // add first city list item cy.contains('button', 'Add Hotel Locations').click(); cy.contains('button', 'Add Cities').click(); cy.contains('label', 'City').next().type('Washington DC'); cy.contains('label', 'Number of Hotels in City').next().type('5'); cy.contains('button', 'Add City Locations').click(); cy.get('[data-testid="field-Hotel Name"]').should('exist'); // add second city list item cy.contains('button', 'Add Cities').click(); cy.get('[data-testid="list-item-field-Cities"]') .eq(0) .within(() => {}); cy.get('[data-testid="list-item-field-Cities"]').eq(1).as('secondCitiesListControl'); cy.get('@secondCitiesListControl').contains('label', 'City').next().type('Boston'); cy.get('@secondCitiesListControl').contains('button', 'Add City Locations').click(); flushClockAndSave(); assertNotification(notifications.error.missingField); // assert on fields cy.get('[data-testid="list-field-Hotel Locations"]').should('have.class', 'CMS_WidgetList_error'); cy.get('[data-testid="list-item-field-Cities"]').should( 'have.class', 'CMS_WidgetList_ListItem_error', ); cy.get('[data-testid="list-item-field-Cities"]') .eq(0) .within(() => { cy.get('[data-testid="field-City"]').should('not.have.class', 'CMS_Field_error'); cy.get('[data-testid="field-Number of Hotels in City"]').should( 'not.have.class', 'CMS_Field_error', ); cy.get('[data-testid="list-field-City Locations"]').should( 'have.class', 'CMS_WidgetList_error', ); cy.get('[data-testid="field-Hotel Name"]').should('have.class', 'CMS_Field_error'); }); cy.get('[data-testid="list-item-field-Cities"]') .eq(1) .within(() => { cy.get('[data-testid="field-City"]').should('not.have.class', 'CMS_Field_error'); cy.get('[data-testid="field-Number of Hotels in City"]').should( 'have.class', 'CMS_Field_error', ); cy.get('[data-testid="list-field-City Locations"]').should( 'have.class', 'CMS_WidgetList_error', ); cy.get('[data-testid="field-Hotel Name"]').should('have.class', 'CMS_Field_error'); }); // list control aliases cy.contains('label', 'Hotel Name').next().type('The Ritz Carlton'); flushClockAndSave(); assertNotification(notifications.error.missingField); // 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); } export function validateObjectFieldsAndExit(setting: ValidateObjectFieldsProps) { validateObjectFields(setting); exitEditor(); } export function validateNestedObjectFieldsAndExit(setting: ValidateObjectFieldsProps) { validateNestedObjectFields(setting); exitEditor(); } export function validateListFieldsAndExit(setting: Author) { validateListFields(setting); exitEditor(); } export function validateNestedListFieldsAndExit() { validateNestedListFields(); exitEditor(); } export interface AssertFieldValidationErrorProps { message: string; fieldLabel: string; } export function assertFieldValidationError({ message, fieldLabel, }: AssertFieldValidationErrorProps) { cy.contains('label', fieldLabel).siblings('[data-testid="error"]').contains(message); cy.get(`[data-testid="field-${fieldLabel}"]`).should('have.class', 'CMS_Field_error'); } function discardDraft() { cy.get('[data-testid="editor-extra-menu"]', { timeout: 5000 }) .should(_ => {}) .then($el => { if ($el.length) { cy.wait(1000); cy.get('[data-testid="editor-extra-menu"]', { timeout: 5000 }).should('be.enabled'); cy.get('[data-testid="editor-extra-menu"]').click(); cy.get('[data-testid="discard-button"]') .should(_ => {}) .then($el => { if ($el.length) { $el.trigger('click'); cy.get('[data-testid="confirm-button"]').click(); } }); } }); }