From eb813f483a1e86a78d626e0988acb1b5afc2c9c1 Mon Sep 17 00:00:00 2001 From: ana-oprea Date: Wed, 18 Dec 2024 11:32:39 +0200 Subject: [PATCH 1/8] add cypress step --- Jenkinsfile | 72 ++++++++++++++++++++++++++--------------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index fac21805..df4578bc 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -16,42 +16,42 @@ pipeline { stage('Integration tests') { parallel { -// stage('Cypress') { -// allOf { -// when { -// environment name: 'CHANGE_ID', value: '' -// not { branch 'master' } -// not { changelog '.*^Automated release [0-9\\.]+$' } -// not { buildingTag() } -// } -// } -// steps { -// node(label: 'docker') { -// script { -// try { -// sh '''docker pull plone; docker run -d --name="$BUILD_TAG-plone" -e SITE="Plone" -e PROFILES="profile-plone.restapi:blocks" plone fg''' -// sh '''docker pull eeacms/volto-project-ci; docker run -i --name="$BUILD_TAG-cypress" --link $BUILD_TAG-plone:plone -e GIT_NAME=$GIT_NAME -e GIT_BRANCH="$BRANCH_NAME" -e GIT_CHANGE_ID="$CHANGE_ID" eeacms/volto-project-ci cypress''' -// } finally { -// try { -// sh '''rm -rf cypress-reports cypress-results''' -// sh '''mkdir -p cypress-reports cypress-results''' -// sh '''docker cp $BUILD_TAG-cypress:/opt/frontend/my-volto-project/cypress/videos cypress-reports/''' -// sh '''docker cp $BUILD_TAG-cypress:/opt/frontend/my-volto-project/cypress/reports cypress-results/''' -// archiveArtifacts artifacts: 'cypress-reports/videos/*.mp4', fingerprint: true -// } -// finally { -// catchError(buildResult: 'SUCCESS', stageResult: 'SUCCESS') { -// junit testResults: 'cypress-results/**/*.xml', allowEmptyResults: true -// } -// sh script: "docker stop $BUILD_TAG-plone", returnStatus: true -// sh script: "docker rm -v $BUILD_TAG-plone", returnStatus: true -// sh script: "docker rm -v $BUILD_TAG-cypress", returnStatus: true -// } -// } -// } -// } -// } -// } + stage('Cypress') { + allOf { + when { + environment name: 'CHANGE_ID', value: '' + not { branch 'master' } + not { changelog '.*^Automated release [0-9\\.]+$' } + not { buildingTag() } + } + } + steps { + node(label: 'docker') { + script { + try { + sh '''docker pull plone; docker run -d --name="$BUILD_TAG-plone" -e SITE="Plone" -e PROFILES="profile-plone.restapi:blocks" plone fg''' + sh '''docker pull eeacms/volto-project-ci; docker run -i --name="$BUILD_TAG-cypress" --link $BUILD_TAG-plone:plone -e GIT_NAME=$GIT_NAME -e GIT_BRANCH="$BRANCH_NAME" -e GIT_CHANGE_ID="$CHANGE_ID" eeacms/volto-project-ci cypress''' + } finally { + try { + sh '''rm -rf cypress-reports cypress-results''' + sh '''mkdir -p cypress-reports cypress-results''' + sh '''docker cp $BUILD_TAG-cypress:/opt/frontend/my-volto-project/cypress/videos cypress-reports/''' + sh '''docker cp $BUILD_TAG-cypress:/opt/frontend/my-volto-project/cypress/reports cypress-results/''' + archiveArtifacts artifacts: 'cypress-reports/videos/*.mp4', fingerprint: true + } + finally { + catchError(buildResult: 'SUCCESS', stageResult: 'SUCCESS') { + junit testResults: 'cypress-results/**/*.xml', allowEmptyResults: true + } + sh script: "docker stop $BUILD_TAG-plone", returnStatus: true + sh script: "docker rm -v $BUILD_TAG-plone", returnStatus: true + sh script: "docker rm -v $BUILD_TAG-cypress", returnStatus: true + } + } + } + } + } + } stage("Docker test build") { when { From f692fa1925a4130092433145b49a8470882617dd Mon Sep 17 00:00:00 2001 From: ana-oprea Date: Wed, 18 Dec 2024 11:43:39 +0200 Subject: [PATCH 2/8] fix Jenkinsfile --- Jenkinsfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index df4578bc..0ffd3342 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -17,8 +17,8 @@ pipeline { stage('Integration tests') { parallel { stage('Cypress') { - allOf { - when { + when { + allOf { environment name: 'CHANGE_ID', value: '' not { branch 'master' } not { changelog '.*^Automated release [0-9\\.]+$' } From 3b38b83ae056b9076676f4d5f3da49c7c1860b6a Mon Sep 17 00:00:00 2001 From: ana-oprea Date: Wed, 18 Dec 2024 11:52:13 +0200 Subject: [PATCH 3/8] add cypress tests --- cypress/e2e/01-block-basics.cy.js | 31 ++ cypress/e2e/01-block-columns.cy.js | 60 ++++ cypress/support/commands.js | 542 ++++++++++++++++++++++++++++- cypress/support/e2e.js | 132 ++++++- cypress/support/index.js | 14 + 5 files changed, 762 insertions(+), 17 deletions(-) create mode 100644 cypress/e2e/01-block-basics.cy.js create mode 100644 cypress/e2e/01-block-columns.cy.js create mode 100644 cypress/support/index.js diff --git a/cypress/e2e/01-block-basics.cy.js b/cypress/e2e/01-block-basics.cy.js new file mode 100644 index 00000000..86270736 --- /dev/null +++ b/cypress/e2e/01-block-basics.cy.js @@ -0,0 +1,31 @@ +import { slateBeforeEach, slateAfterEach } from '../support/e2e'; + +describe('Blocks Tests', () => { + beforeEach(slateBeforeEach); + afterEach(slateAfterEach); + + it('Add Block: Empty', () => { + // Change page title + cy.clearSlateTitle(); + cy.getSlateTitle().type('My Add-on Page'); + + cy.get('.documentFirstHeading').contains('My Add-on Page'); + + cy.getSlate().click({ force: true }); + + // Add block + cy.get('.ui.basic.icon.button.block-add-button') + .first() + .click({ force: true }); + cy.get('.blocks-chooser .title').contains('Media').click(); + cy.get('.content.active.media .button.image').contains('Image').click(); + + // Save + cy.get('#toolbar-save').click(); + cy.url().should('eq', Cypress.config().baseUrl + '/cypress/my-page'); + + // then the page view should contain our changes + cy.contains('My Add-on Page'); + cy.get('.block.image'); + }); +}); diff --git a/cypress/e2e/01-block-columns.cy.js b/cypress/e2e/01-block-columns.cy.js new file mode 100644 index 00000000..f51b089a --- /dev/null +++ b/cypress/e2e/01-block-columns.cy.js @@ -0,0 +1,60 @@ +import { slateBeforeEach, slateAfterEach } from '../support/e2e'; + +describe('Blocks Tests', () => { + beforeEach(slateBeforeEach); + afterEach(slateAfterEach); + + it('Add Block: Empty', () => { + // Change page title + cy.clearSlateTitle(); + cy.getSlateTitle().type('My Add-on Page'); + + cy.get('.documentFirstHeading').contains('My Add-on Page'); + + cy.getSlate().click({ force: true }); + + // Add block + cy.get('.ui.basic.icon.button.block-add-button') + .first() + .click({ force: true }); + cy.get('.blocks-chooser .title').contains('Common').click(); + cy.get('.content.active.common .button.columnsBlock') + .contains('Columns') + .click({ force: true }); + + cy.get('.columns-block .ui.card').eq(2).click({ force: true }); + cy.get('.field-wrapper-title #field-title').last().type('Column test'); + cy.get('.field-wrapper-data .columns-area button').last().click(); + + cy.get('.columns-area .drag.handle.wrapper') + .first() + .trigger('mousedown', { which: 1 }, { force: true }) + .trigger('mousemove', 0, 60, { force: true }) + .trigger('mouseup'); + + cy.get('.field-wrapper-gridCols #field-gridCols').click(); + cy.get('.react-select__menu').contains('25').click(); + + cy.get('[contenteditable=true]').first().focus().click(); + cy.get('.columns-block [contenteditable=true]') + .eq(0) + .focus() + .click() + .type('First'); + cy.get('.columns-block [contenteditable=true]') + .eq(1) + .focus() + .click() + .type('Second'); + + // Save + cy.get('#toolbar-save').click(); + cy.url().should('eq', Cypress.config().baseUrl + '/cypress/my-page'); + + // then the page view should contain our changes + cy.contains('My Add-on Page'); + cy.contains('First'); + cy.contains('Second'); + cy.get('.columns-view'); + }); +}); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 03fe48c7..e9f51b16 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -1,9 +1,539 @@ -import '@plone/volto/cypress/add-commands'; +/* eslint no-console: ["error", { allow: ["log"] }] */ -// Set PLONE_SITE_ID and PLONE_API_URL as cypress environment variables -// if testing without using localhost or a site id of `plone`. +const SLATE_SELECTOR = '.content-area .slate-editor [contenteditable=true]'; +const SLATE_TITLE_SELECTOR = '.block.inner.title [contenteditable="true"]'; -// --- CUSTOM COMMANDS ------------------------------------------------------------- -Cypress.Commands.add('custom_command', () => { - // Custom code here... +// --- AUTOLOGIN ------------------------------------------------------------- +Cypress.Commands.add('autologin', () => { + let api_url, user, password; + api_url = Cypress.env('API_PATH') || 'http://localhost:8080/Plone'; + user = 'admin'; + password = 'admin'; + + return cy + .request({ + method: 'POST', + url: `${api_url}/@login`, + headers: { Accept: 'application/json' }, + body: { login: user, password: password }, + }) + .then((response) => cy.setCookie('auth_token', response.body.token)); +}); + +// --- CREATE CONTENT -------------------------------------------------------- +Cypress.Commands.add( + 'createContent', + ({ + contentType, + contentId, + contentTitle, + path = '', + allow_discussion = false, + }) => { + let api_url, auth; + api_url = Cypress.env('API_PATH') || 'http://localhost:8080/Plone'; + auth = { + user: 'admin', + pass: 'admin', + }; + if (contentType === 'File') { + return cy.request({ + method: 'POST', + url: `${api_url}/${path}`, + headers: { + Accept: 'application/json', + }, + auth: auth, + body: { + '@type': contentType, + id: contentId, + title: contentTitle, + file: { + data: 'dGVzdGZpbGUK', + encoding: 'base64', + filename: 'lorem.txt', + 'content-type': 'text/plain', + }, + allow_discussion: allow_discussion, + }, + }); + } + if (contentType === 'Image') { + return cy.request({ + method: 'POST', + url: `${api_url}/${path}`, + headers: { + Accept: 'application/json', + }, + auth: auth, + body: { + '@type': contentType, + id: contentId, + title: contentTitle, + image: { + data: 'iVBORw0KGgoAAAANSUhEUgAAANcAAAA4CAMAAABZsZ3QAAAAM1BMVEX29fK42OU+oMvn7u9drtIPisHI4OhstdWZyt4fkcXX5+sAg74umMhNp86p0eJ7vNiKw9v/UV4wAAAAAXRSTlMAQObYZgAABBxJREFUeF7tmuty4yAMhZG4X2zn/Z92J5tsBJwWXG/i3XR6frW2Y/SBLIRAfaQUDNt8E5tLUt9BycfcKfq3R6Mlfyimtx4rzp+K3dtibXkor99zsEqLYZltblTecciogoh+TXfY1Ve4dn07rCDGG9dHSEEOg/GmXl0U1XDxTKxNK5De7BxsyyBr6gGm2/vPxKJ8F6f7BXKfRMp1xIWK9A+5ks25alSb353dWnDJN1k35EL5f8dVGifTf/4tjUuuFq7u4srmXC60yAmldLXIWbg65RKU87lcGxJCFqUPv0IacW0PmSivOZFLE908inPToMmii/roG+MRV/O8FU88i8tFsxV3a06MFUw0Qu7RmAtdV5/HVVaOVMTWNOWSwMljLhzhcB6XIS7OK5V6AvRDNN7t5VJWQs1J40UmalbK56usBG/CuCHSYuc+rkUGeMCViNRARPrzW52N3oQLe6WifNliSuuGaH3czbVNudI9s7ZLUCLHVwWlyES522o1t14uvmbblmVTKqFjaZYJFSTPP4dLL1kU1z7p0lzdbRulmEWLxoQX+z9ce7A8GqEEucllLxePuZwdJl1Lezu0hoswvTPt61DrFcRuujV/2cmlxaGBC7Aw6cpovGANwRiSdOAWJ5AGy4gLL64dl0QhUEAuEUNws+XxV+OKGPdw/hESGYF9XEGaFC7sNLMSXWJjHsnanYi87VK428N2uxpOjOFANcagLM5l+7mSycM8KknZpKLcGi6jmzWGr/vLurZ/0g4u9AZuAoeb5r1ceQhyiTPY1E4wUR6u/F3H2ojSpXMMriBPT9cezTto8Cx+MsglHL4fv1Rxrb1LVw9yvyQpJ3AhFnLZfuRLH2QsOG3FGGD20X/th/u5bFAt16Bt308KjF+MNOXgl/SquIEySX3GhaZvc67KZbDxcCDORz2N8yCWPaY5lyQZO7lQ29fnZbt3Xu6qoge4+DjXl/MocySPOp9rlvdyznahRyHEYd77v3LhugOXDv4J65QXfl803BDAdaWBEDhfVx7nKofjoVCgxnUAqw/UAUDPn788BDvQuG4TDtdtUPvzjSlXAB8DvaDOhhrmhwbywylXAm8CvaouikJTL93gs3y7Yy4VYbIxOHrcMizPqWOjqO9l3Uz52kibQy4xxOgqhJvD+w5rvokOcAlGvNCfeqCv1ste1stzLm0f71Iq3ZfTrPfuE5nhPtF+LvQE2lffQC7pYtQy3tdzdrKvd5TLVVzDetScS3nEKmmwDyt1Cev1kX3YfbvzNK4fzrlw+cB6vm+uiUgf2zdXI62241LawCb7Pi5FXFPF8KpzDoF/Sw2lg+GrHNbno1mhPu+VCF/vfMnw06PnUl6j48dVHD3jHNHPua+fc3o/5yp/zsGi0vYtzi3Pz5mHd4T6BWMIlewacd63AAAAAElFTkSuQmCC', + encoding: 'base64', + filename: 'image.png', + 'content-type': 'image/png', + }, + }, + }); + } + if (['Document', 'Folder', 'CMSFolder'].includes(contentType)) { + return cy + .request({ + method: 'POST', + url: `${api_url}/${path}`, + headers: { + Accept: 'application/json', + }, + auth: auth, + body: { + '@type': contentType, + id: contentId, + title: contentTitle, + blocks: { + 'd3f1c443-583f-4e8e-a682-3bf25752a300': { '@type': 'title' }, + '7624cf59-05d0-4055-8f55-5fd6597d84b0': { '@type': 'slate' }, + }, + blocks_layout: { + items: [ + 'd3f1c443-583f-4e8e-a682-3bf25752a300', + '7624cf59-05d0-4055-8f55-5fd6597d84b0', + ], + }, + allow_discussion: allow_discussion, + }, + }) + .then(() => console.log(`${contentType} created`)); + } else { + return cy + .request({ + method: 'POST', + url: `${api_url}/${path}`, + headers: { + Accept: 'application/json', + }, + auth: auth, + body: { + '@type': contentType, + id: contentId, + title: contentTitle, + allow_discussion: allow_discussion, + }, + }) + .then(() => console.log(`${contentType} created`)); + } + }, +); + +// --- Add DX Content-Type ---------------------------------------------------------- +Cypress.Commands.add('addContentType', (name) => { + let api_url, auth; + api_url = Cypress.env('API_PATH') || 'http://localhost:8080/Plone'; + auth = { + user: 'admin', + pass: 'admin', + }; + return cy + .request({ + method: 'POST', + url: `${api_url}/@controlpanels/dexterity-types/${name}`, + headers: { + Accept: 'application/json', + }, + auth: auth, + body: { + title: name, + }, + }) + .then(() => console.log(`${name} content-type added.`)); +}); + +// --- Remove DX behavior ---------------------------------------------------------- +Cypress.Commands.add('removeContentType', (name) => { + let api_url, auth; + api_url = Cypress.env('API_PATH') || 'http://localhost:8080/Plone'; + auth = { + user: 'admin', + pass: 'admin', + }; + return cy + .request({ + method: 'DELETE', + url: `${api_url}/@controlpanels/dexterity-types/${name}`, + headers: { + Accept: 'application/json', + }, + auth: auth, + body: {}, + }) + .then(() => console.log(`${name} content-type removed.`)); +}); + +// --- Add DX field ---------------------------------------------------------- +Cypress.Commands.add('addSlateJSONField', (type, name) => { + let api_url, auth; + api_url = Cypress.env('API_PATH') || 'http://localhost:8080/Plone'; + auth = { + user: 'admin', + pass: 'admin', + }; + return cy + .request({ + method: 'POST', + url: `${api_url}/@types/${type}`, + headers: { + Accept: 'application/json', + }, + auth: auth, + body: { + id: name, + title: name, + description: 'Slate JSON Field', + factory: 'SlateJSONField', + required: false, + }, + }) + .then(() => console.log(`${name} SlateJSONField field added to ${type}`)); +}); + +// --- Remove DX field ---------------------------------------------------------- +Cypress.Commands.add('removeSlateJSONField', (type, name) => { + let api_url, auth; + api_url = Cypress.env('API_PATH') || 'http://localhost:8080/Plone'; + auth = { + user: 'admin', + pass: 'admin', + }; + return cy + .request({ + method: 'DELETE', + url: `${api_url}/@types/${type}/${name}`, + headers: { + Accept: 'application/json', + }, + auth: auth, + body: {}, + }) + .then(() => + console.log(`${name} SlateJSONField field removed from ${type}`), + ); +}); + +// --- REMOVE CONTENT -------------------------------------------------------- +Cypress.Commands.add('removeContent', (path) => { + let api_url, auth; + api_url = Cypress.env('API_PATH') || 'http://localhost:8080/Plone'; + auth = { + user: 'admin', + pass: 'admin', + }; + return cy + .request({ + method: 'DELETE', + url: `${api_url}/${path}`, + headers: { + Accept: 'application/json', + }, + auth: auth, + body: {}, + }) + .then(() => console.log(`${path} removed`)); +}); + +Cypress.Commands.add('typeInSlate', { prevSubject: true }, (subject, text) => { + return ( + cy + .wrap(subject) + .then((subject) => { + subject[0].dispatchEvent( + new InputEvent('beforeinput', { + inputType: 'insertText', + data: text, + }), + ); + return subject; + }) + // TODO: do this only for Electron-based browser which does not understand instantaneously + // that the user inserted some text in the block + .wait(1000) + ); +}); + +Cypress.Commands.add('lineBreakInSlate', { prevSubject: true }, (subject) => { + return ( + cy + .wrap(subject) + .then((subject) => { + subject[0].dispatchEvent( + new InputEvent('beforeinput', { inputType: 'insertLineBreak' }), + ); + return subject; + }) + // TODO: do this only for Electron-based browser which does not understand instantaneously + // that the block was split + .wait(1000) + ); +}); + +// --- SET WORKFLOW ---------------------------------------------------------- +Cypress.Commands.add( + 'setWorkflow', + ({ + path = '/', + actor = 'admin', + review_state = 'publish', + time = '1995-07-31T18:30:00', + title = '', + comment = '', + effective = '2018-01-21T08:00:00', + expires = '2019-01-21T08:00:00', + include_children = true, + }) => { + let api_url, auth; + api_url = Cypress.env('API_PATH') || 'http://localhost:8080/Plone'; + auth = { + user: 'admin', + pass: 'admin', + }; + return cy.request({ + method: 'POST', + url: `${api_url}/${path}/@workflow/${review_state}`, + headers: { + Accept: 'application/json', + }, + auth: auth, + body: { + actor: actor, + review_state: review_state, + time: time, + title: title, + comment: comment, + effective: effective, + expires: expires, + include_children: include_children, + }, + }); + }, +); + +// --- waitForResourceToLoad ---------------------------------------------------------- +Cypress.Commands.add('waitForResourceToLoad', (fileName, type) => { + const resourceCheckInterval = 40; + + return new Cypress.Promise((resolve) => { + const checkIfResourceHasBeenLoaded = () => { + const resource = cy + .state('window') + .performance.getEntriesByType('resource') + .filter((entry) => !type || entry.initiatorType === type) + .find((entry) => entry.name.includes(fileName)); + + if (resource) { + resolve(); + + return; + } + + setTimeout(checkIfResourceHasBeenLoaded, resourceCheckInterval); + }; + + checkIfResourceHasBeenLoaded(); + }); +}); + +// Low level command reused by `setSelection` and low level command `setCursor` +Cypress.Commands.add('selection', { prevSubject: true }, (subject, fn) => { + cy.wrap(subject) + .trigger('mousedown', { force: true }) + .then(fn) + .trigger('mouseup'); + + cy.document().trigger('selectionchange'); + return cy.wrap(subject); +}); + +Cypress.Commands.add( + 'setSelection', + { prevSubject: true }, + (subject, query, endQuery) => { + return cy.wrap(subject).selection(($el) => { + if (typeof query === 'string') { + const anchorNode = getTextNode($el[0], query); + const focusNode = endQuery ? getTextNode($el[0], endQuery) : anchorNode; + const anchorOffset = anchorNode.wholeText.indexOf(query); + const focusOffset = endQuery + ? focusNode.wholeText.indexOf(endQuery) + endQuery.length + : anchorOffset + query.length; + setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset); + } else if (typeof query === 'object') { + const el = $el[0]; + const anchorNode = getTextNode(el.querySelector(query.anchorQuery)); + const anchorOffset = query.anchorOffset || 0; + const focusNode = query.focusQuery + ? getTextNode(el.querySelector(query.focusQuery)) + : anchorNode; + const focusOffset = query.focusOffset || 0; + setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset); + } + }); + }, +); + +Cypress.Commands.add('getSlate', ({ createNewSlate = true } = {}) => { + let slate; + cy.getIfExists( + SLATE_SELECTOR, + () => { + slate = cy.get(SLATE_SELECTOR).last(); + }, + () => { + if (createNewSlate) { + cy.get('.block.inner').last().type('{moveToEnd}{enter}'); + } + slate = cy.get(SLATE_SELECTOR, { timeout: 10000 }).last(); + }, + ); + return slate; }); + +Cypress.Commands.add('clearSlate', (selector) => { + return cy + .get(selector) + .focus() + .click() + .wait(1000) + .type('{selectAll}') + .wait(1000) + .type('{backspace}'); +}); + +Cypress.Commands.add('getSlateTitle', () => { + return cy.get(SLATE_TITLE_SELECTOR, { + timeout: 10000, + }); +}); + +Cypress.Commands.add('clearSlateTitle', () => { + return cy.clearSlate(SLATE_TITLE_SELECTOR); +}); + +Cypress.Commands.add('setSlateSelection', (subject, query, endQuery) => { + cy.get('.slate-editor.selected [contenteditable=true]') + .focus() + .click() + .setSelection(subject, query, endQuery) + .wait(1000); // this wait is needed for the selection change to be detected after +}); + +Cypress.Commands.add('getSlateEditorAndType', (type) => { + cy.getSlate().focus().click({ force: true }).type(type); +}); + +Cypress.Commands.add('setSlateCursor', (subject, query, endQuery) => { + cy.get('.slate-editor.selected [contenteditable=true]') + .focus() + .click({ force: true }) + .setCursor(subject, query, endQuery) + .wait(1000); +}); + +Cypress.Commands.add('clickSlateButton', (button) => { + cy.get(`.slate-inline-toolbar .button-wrapper a[title="${button}"]`, { + timeout: 10000, + }).click({ force: true }); //force click is needed to ensure the button in visible in view. +}); + +Cypress.Commands.add('toolbarSave', () => { + cy.wait(1000); + + // Save + cy.get('#toolbar-save').click(); + cy.waitForResourceToLoad('@navigation'); + cy.waitForResourceToLoad('@breadcrumbs'); + cy.waitForResourceToLoad('@actions'); + cy.waitForResourceToLoad('@types'); + cy.waitForResourceToLoad('my-page'); + cy.url().should('eq', Cypress.config().baseUrl + '/cypress/my-page'); +}); + +// Low level command reused by `setCursorBefore` and `setCursorAfter`, equal to `setCursorAfter` +Cypress.Commands.add( + 'setCursor', + { prevSubject: true }, + (subject, query, atStart) => { + return cy.wrap(subject).selection(($el) => { + const node = getTextNode($el[0], query); + const offset = + node.wholeText.indexOf(query) + (atStart ? 0 : query.length); + const document = node.ownerDocument; + document.getSelection().removeAllRanges(); + document.getSelection().collapse(node, offset); + }); + // Depending on what you're testing, you may need to chain a `.click()` here to ensure + // further commands are picked up by whatever you're testing (this was required for Slate, for example). + }, +); + +Cypress.Commands.add( + 'setCursorBefore', + { prevSubject: true }, + (subject, query) => { + cy.wrap(subject).setCursor(query, true); + }, +); + +Cypress.Commands.add( + 'setCursorAfter', + { prevSubject: true }, + (subject, query) => { + cy.wrap(subject).setCursor(query); + }, +); + +// Helper functions +function getTextNode(el, match) { + const walk = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null, false); + if (!match) { + return walk.nextNode(); + } + + let node; + while ((node = walk.nextNode())) { + if (node.wholeText.includes(match)) { + return node; + } + } +} + +function setBaseAndExtent(...args) { + const document = args[0].ownerDocument; + document.getSelection().removeAllRanges(); + document.getSelection().setBaseAndExtent(...args); +} + +Cypress.Commands.add('navigate', (route = '') => { + return cy.window().its('appHistory').invoke('push', route); +}); + +Cypress.Commands.add('store', () => { + return cy.window().its('store').invoke('getStore', ''); +}); + +Cypress.Commands.add('settings', (key, value) => { + return cy.window().its('settings'); +}); + +Cypress.Commands.add( + 'getIfExists', + (selector, successAction = () => {}, failAction = () => {}) => { + cy.get('body').then((body) => { + if (body.find(selector).length > 0 && successAction) { + successAction(); + } else if (failAction) { + failAction(); + } + }); + }, +); diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js index 48912f7f..b351da5b 100644 --- a/cypress/support/e2e.js +++ b/cypress/support/e2e.js @@ -1,14 +1,124 @@ -import 'cypress-axe'; -import 'cypress-file-upload'; +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: import './commands'; -import { setup, teardown } from './reset-fixture'; +// Alternatively you can use CommonJS syntax: +// require('./commands') + +//Generate code-coverage +import '@cypress/code-coverage/support'; + +// Fail Fast +import 'cypress-fail-fast'; + +export const slateBeforeEach = (contentType = 'Document') => { + cy.intercept('GET', `/**/*?expand*`).as('content'); + cy.autologin(); + cy.createContent({ + contentType: 'Document', + contentId: 'cypress', + contentTitle: 'Cypress', + }); + cy.createContent({ + contentType: contentType, + contentId: 'my-page', + contentTitle: 'My Page', + path: 'cypress', + }); + cy.visit('/cypress/my-page'); + cy.navigate('/cypress/my-page/edit'); +}; + +export const slateAfterEach = () => { + cy.autologin(); + cy.removeContent('cypress'); +}; + +export const slateJsonBeforeEach = (contentType = 'slate') => { + cy.autologin(); + cy.addContentType(contentType); + cy.addSlateJSONField(contentType, 'slate'); + slateBeforeEach(contentType); +}; + +export const slateJsonAfterEach = (contentType = 'slate') => { + cy.autologin(); + cy.removeContentType(contentType); + slateAfterEach(); +}; + +export const getSelectedSlateEditor = () => { + return cy.get('.slate-editor.selected [contenteditable=true]').click(); +}; + +export const createSlateBlock = () => { + cy.get('.ui.basic.icon.button.block-add-button').first().click(); + cy.get('.blocks-chooser .title').contains('Text').click(); + cy.get('.ui.basic.icon.button.slate').contains('Text').click(); + return getSelectedSlateEditor(); +}; + +export const getSlateBlockValue = (sb) => { + return sb.invoke('attr', 'data-slate-value').then((str) => { + return typeof str === 'undefined' ? [] : JSON.parse(str); + }); +}; + +export const createSlateBlockWithList = ({ + numbered, + firstItemText, + secondItemText, +}) => { + let s1 = createSlateBlock(); + + s1.typeInSlate(firstItemText + secondItemText); + + // select all contents of slate block + // - this opens hovering toolbar + cy.contains(firstItemText + secondItemText).then((el) => { + selectSlateNodeOfWord(el); + }); + + // TODO: do not hardcode these selectors: + if (numbered) { + // this is the numbered list option in the hovering toolbar + cy.get('.slate-inline-toolbar > :nth-child(9)').click(); + } else { + // this is the bulleted list option in the hovering toolbar + cy.get('.slate-inline-toolbar > :nth-child(10)').click(); + } + + // move the text cursor + const sse = getSelectedSlateEditor(); + sse.type('{leftarrow}'); + for (let i = 0; i < firstItemText.length; ++i) { + sse.type('{rightarrow}'); + } + + // simulate pressing Enter + getSelectedSlateEditor().lineBreakInSlate(); -beforeEach(function () { - cy.log('Setting up API fixture'); - setup(); -}); + return s1; +}; -afterEach(function () { - cy.log('Tearing down API fixture'); - teardown(); -}); +export const selectSlateNodeOfWord = (el) => { + return cy.window().then((win) => { + var event = new CustomEvent('Test_SelectWord', { + detail: el[0], + }); + win.document.dispatchEvent(event); + }); +}; diff --git a/cypress/support/index.js b/cypress/support/index.js new file mode 100644 index 00000000..48912f7f --- /dev/null +++ b/cypress/support/index.js @@ -0,0 +1,14 @@ +import 'cypress-axe'; +import 'cypress-file-upload'; +import './commands'; +import { setup, teardown } from './reset-fixture'; + +beforeEach(function () { + cy.log('Setting up API fixture'); + setup(); +}); + +afterEach(function () { + cy.log('Tearing down API fixture'); + teardown(); +}); From 63c8f9878caea3db11973908fe997c22a397f9d0 Mon Sep 17 00:00:00 2001 From: ana-oprea Date: Wed, 18 Dec 2024 11:55:52 +0200 Subject: [PATCH 4/8] fix Jenkinsfile --- Jenkinsfile | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 0ffd3342..6bcba550 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -18,13 +18,14 @@ pipeline { parallel { stage('Cypress') { when { - allOf { - environment name: 'CHANGE_ID', value: '' - not { branch 'master' } - not { changelog '.*^Automated release [0-9\\.]+$' } - not { buildingTag() } - } - } + allOf { + environment name: 'CHANGE_ID', value: '' + anyOf { + not { changelog '.*^Automated release [0-9\\.]+$' } + branch 'master' + } + } + } steps { node(label: 'docker') { script { From 03f4f2ae4bd72538f14094303d29ea73e2bf4922 Mon Sep 17 00:00:00 2001 From: valentinab25 <30239069+valentinab25@users.noreply.github.com> Date: Wed, 18 Dec 2024 15:52:33 +0200 Subject: [PATCH 5/8] test: [JENKINS] run tests on PR to develop --- Jenkinsfile | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 6bcba550..0bbef639 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -18,13 +18,20 @@ pipeline { parallel { stage('Cypress') { when { + anyOf { + allOf { + not { environment name: 'CHANGE_ID', value: '' } + environment name: 'CHANGE_TARGET', value: 'develop' + } allOf { environment name: 'CHANGE_ID', value: '' anyOf { not { changelog '.*^Automated release [0-9\\.]+$' } branch 'master' + branch 'develop' } } + } } steps { node(label: 'docker') { @@ -223,4 +230,4 @@ pipeline { } } } -} \ No newline at end of file +} From af001454c98b202865293c099801109bca1aebcd Mon Sep 17 00:00:00 2001 From: ana-oprea Date: Wed, 18 Dec 2024 16:11:03 +0200 Subject: [PATCH 6/8] update cypress.config.js --- cypress.config.js | 18 ++++++++++++++++-- cypress/tests/.gitkeep | 0 2 files changed, 16 insertions(+), 2 deletions(-) delete mode 100644 cypress/tests/.gitkeep diff --git a/cypress.config.js b/cypress.config.js index 08d55e62..aaf03bfc 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -1,8 +1,22 @@ const { defineConfig } = require('cypress'); - module.exports = defineConfig({ + viewportWidth: 1280, + defaultCommandTimeout: 15000, + chromeWebSecurity: false, + + reporter: 'junit', + video: true, + reporterOptions: { + mochaFile: 'cypress/reports/cypress-[hash].xml', + jenkinsMode: true, + toConsole: true, + }, e2e: { + setupNodeEvents(on, config) { + // e2e testing node events setup code + + return config; + }, baseUrl: 'http://localhost:3000', - specPattern: 'cypress/tests/**/*.cy.{js,jsx}', }, }); diff --git a/cypress/tests/.gitkeep b/cypress/tests/.gitkeep deleted file mode 100644 index e69de29b..00000000 From a755c469a491fc88ec6a94b4f0c79679f5e46b4e Mon Sep 17 00:00:00 2001 From: ana-oprea Date: Wed, 18 Dec 2024 16:26:59 +0200 Subject: [PATCH 7/8] fix support/e2e.js --- cypress/support/e2e.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js index b351da5b..84b53726 100644 --- a/cypress/support/e2e.js +++ b/cypress/support/e2e.js @@ -19,10 +19,10 @@ import './commands'; // require('./commands') //Generate code-coverage -import '@cypress/code-coverage/support'; +// import '@cypress/code-coverage/support'; -// Fail Fast -import 'cypress-fail-fast'; +// // Fail Fast +// import 'cypress-fail-fast'; export const slateBeforeEach = (contentType = 'Document') => { cy.intercept('GET', `/**/*?expand*`).as('content'); From eef0b66d0eb425c4f371dc219d2aa0718f72143e Mon Sep 17 00:00:00 2001 From: ana-oprea Date: Wed, 18 Dec 2024 17:21:27 +0200 Subject: [PATCH 8/8] fix Jenkinsfile --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 0bbef639..2021208c 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -37,7 +37,7 @@ pipeline { node(label: 'docker') { script { try { - sh '''docker pull plone; docker run -d --name="$BUILD_TAG-plone" -e SITE="Plone" -e PROFILES="profile-plone.restapi:blocks" plone fg''' + sh '''docker pull eeacms/plone-backend; docker run --rm -d --name="$BUILD_TAG-plone" -e SITE="Plone" -e PROFILES="eea.kitkat:testing" eeacms/plone-backend''' sh '''docker pull eeacms/volto-project-ci; docker run -i --name="$BUILD_TAG-cypress" --link $BUILD_TAG-plone:plone -e GIT_NAME=$GIT_NAME -e GIT_BRANCH="$BRANCH_NAME" -e GIT_CHANGE_ID="$CHANGE_ID" eeacms/volto-project-ci cypress''' } finally { try {