diff --git a/.coverage.babel.config.js b/.coverage.babel.config.js
new file mode 100644
index 0000000..e8b54d3
--- /dev/null
+++ b/.coverage.babel.config.js
@@ -0,0 +1,9 @@
+const defaultBabel = require('@plone/volto/babel');
+
+function applyDefault(api) {
+ const voltoBabel = defaultBabel(api);
+ voltoBabel.plugins.push('@babel/plugin-transform-modules-commonjs', 'transform-class-properties', 'istanbul');
+ return voltoBabel;
+}
+
+module.exports = applyDefault;
diff --git a/Jenkinsfile b/Jenkinsfile
index f72cbd8..2b75e10 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -5,6 +5,7 @@ pipeline {
GIT_NAME = "volto-widget-temporal-coverage"
NAMESPACE = "@eeacms"
SONARQUBE_TAGS = "volto.eea.europa.eu"
+ DEPENDENCIES = ""
}
stages {
@@ -44,13 +45,13 @@ pipeline {
try {
sh '''docker pull plone/volto-addon-ci'''
sh '''docker run -i --name="$BUILD_TAG-volto" -e NAMESPACE="$NAMESPACE" -e GIT_NAME=$GIT_NAME -e GIT_BRANCH="$BRANCH_NAME" -e GIT_CHANGE_ID="$CHANGE_ID" plone/volto-addon-ci'''
+ sh '''rm -rf xunit-reports'''
sh '''mkdir -p xunit-reports'''
sh '''docker cp $BUILD_TAG-volto:/opt/frontend/my-volto-project/coverage xunit-reports/'''
sh '''docker cp $BUILD_TAG-volto:/opt/frontend/my-volto-project/junit.xml xunit-reports/'''
sh '''docker cp $BUILD_TAG-volto:/opt/frontend/my-volto-project/unit_tests_log.txt xunit-reports/'''
- stash name: "xunit-reports", includes: "xunit-reports/**/*"
- junit 'xunit-reports/junit.xml'
- archiveArtifacts artifacts: 'xunit-reports/unit_tests_log.txt', fingerprint: true
+ stash name: "xunit-reports", includes: "xunit-reports/**"
+ archiveArtifacts artifacts: "xunit-reports/unit_tests_log.txt", fingerprint: true
publishHTML (target : [
allowMissing: false,
alwaysLinkToLastBuild: true,
@@ -61,7 +62,10 @@ pipeline {
reportTitles: 'Unit Tests Code Coverage'
])
} finally {
- sh '''docker rm -v $BUILD_TAG-volto'''
+ catchError(buildResult: 'SUCCESS', stageResult: 'SUCCESS') {
+ junit testResults: 'xunit-reports/junit.xml', allowEmptyResults: true
+ }
+ sh script: '''docker rm -v $BUILD_TAG-volto''', returnStatus: true
}
}
}
@@ -70,6 +74,53 @@ pipeline {
}
}
+ stage('Integration tests') {
+ steps {
+ parallel(
+
+ "Cypress": {
+ 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 plone/volto-addon-ci; docker run -i --name="$BUILD_TAG-cypress" --link $BUILD_TAG-plone:plone -e NAMESPACE="$NAMESPACE" -e GIT_NAME=$GIT_NAME -e GIT_BRANCH="$BRANCH_NAME" -e GIT_CHANGE_ID="$CHANGE_ID" -e DEPENDENCIES="$DEPENDENCIES" plone/volto-addon-ci cypress'''
+ } finally {
+ try {
+ sh '''rm -rf cypress-reports cypress-results cypress-coverage'''
+ sh '''mkdir -p cypress-reports cypress-results cypress-coverage'''
+ sh '''docker cp $BUILD_TAG-cypress:/opt/frontend/my-volto-project/src/addons/$GIT_NAME/cypress/videos cypress-reports/'''
+ sh '''docker cp $BUILD_TAG-cypress:/opt/frontend/my-volto-project/src/addons/$GIT_NAME/cypress/reports cypress-results/'''
+ coverage = sh script: '''docker cp $BUILD_TAG-cypress:/opt/frontend/my-volto-project/src/addons/$GIT_NAME/coverage cypress-coverage/''', returnStatus: true
+ if ( coverage == 0 ) {
+ publishHTML (target : [allowMissing: false,
+ alwaysLinkToLastBuild: true,
+ keepAll: true,
+ reportDir: 'cypress-coverage/coverage/lcov-report',
+ reportFiles: 'index.html',
+ reportName: 'CypressCoverage',
+ reportTitles: 'Integration Tests Code Coverage'])
+ }
+ archiveArtifacts artifacts: 'cypress-reports/videos/*.mp4', fingerprint: true
+ stash name: "cypress-coverage", includes: "cypress-coverage/**", allowEmpty: 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('Report to SonarQube') {
// Exclude Pull-Requests
when {
@@ -82,11 +133,12 @@ pipeline {
script{
checkout scm
unstash "xunit-reports"
+ unstash "cypress-coverage"
def scannerHome = tool 'SonarQubeScanner';
def nodeJS = tool 'NodeJS11';
withSonarQubeEnv('Sonarqube') {
sh '''sed -i "s#/opt/frontend/my-volto-project/src/addons/${GIT_NAME}/##g" xunit-reports/coverage/lcov.info'''
- sh "export PATH=$PATH:${scannerHome}/bin:${nodeJS}/bin; sonar-scanner -Dsonar.javascript.lcov.reportPaths=./xunit-reports/coverage/lcov.info -Dsonar.sources=./src -Dsonar.coverage.exclusions=src/**/*.test.js -Dsonar.projectKey=$GIT_NAME-$BRANCH_NAME -Dsonar.projectVersion=$BRANCH_NAME-$BUILD_NUMBER"
+ sh "export PATH=$PATH:${scannerHome}/bin:${nodeJS}/bin; sonar-scanner -Dsonar.javascript.lcov.reportPaths=./xunit-reports/coverage/lcov.info,./cypress-coverage/coverage/lcov.info -Dsonar.sources=./src -Dsonar.projectKey=$GIT_NAME-$BRANCH_NAME -Dsonar.projectVersion=$BRANCH_NAME-$BUILD_NUMBER"
sh '''try=2; while [ \$try -gt 0 ]; do curl -s -XPOST -u "${SONAR_AUTH_TOKEN}:" "${SONAR_HOST_URL}api/project_tags/set?project=${GIT_NAME}-${BRANCH_NAME}&tags=${SONARQUBE_TAGS},${BRANCH_NAME}" > set_tags_result; if [ \$(grep -ic error set_tags_result ) -eq 0 ]; then try=0; else cat set_tags_result; echo "... Will retry"; sleep 60; try=\$(( \$try - 1 )); fi; done'''
}
}
@@ -109,7 +161,7 @@ pipeline {
}
withCredentials([string(credentialsId: 'eea-jenkins-token', variable: 'GITHUB_TOKEN')]) {
sh '''docker pull eeacms/gitflow'''
- sh '''docker run -i --rm --name="$BUILD_TAG-gitflow-pr" -e GIT_CHANGE_TARGET="$CHANGE_TARGET" -e GIT_CHANGE_BRANCH="$CHANGE_BRANCH" -e GIT_CHANGE_AUTHOR="$CHANGE_AUTHOR" -e GIT_CHANGE_TITLE="$CHANGE_TITLE" -e GIT_TOKEN="$GITHUB_TOKEN" -e GIT_BRANCH="$BRANCH_NAME" -e GIT_CHANGE_ID="$CHANGE_ID" -e GIT_ORG="$GIT_ORG" -e GIT_NAME="$GIT_NAME" eeacms/gitflow'''
+ sh '''docker run -i --rm --name="$BUILD_TAG-gitflow-pr" -e GIT_CHANGE_TARGET="$CHANGE_TARGET" -e GIT_CHANGE_BRANCH="$CHANGE_BRANCH" -e GIT_CHANGE_AUTHOR="$CHANGE_AUTHOR" -e GIT_CHANGE_TITLE="$CHANGE_TITLE" -e GIT_TOKEN="$GITHUB_TOKEN" -e GIT_BRANCH="$BRANCH_NAME" -e GIT_CHANGE_ID="$CHANGE_ID" -e GIT_ORG="$GIT_ORG" -e GIT_NAME="$GIT_NAME" -e LANGUAGE=javascript eeacms/gitflow'''
}
}
}
@@ -127,7 +179,7 @@ pipeline {
node(label: 'docker') {
withCredentials([string(credentialsId: 'eea-jenkins-token', variable: 'GITHUB_TOKEN'),string(credentialsId: 'eea-jenkins-npm-token', variable: 'NPM_TOKEN')]) {
sh '''docker pull eeacms/gitflow'''
- sh '''docker run -i --rm --name="$BUILD_TAG-gitflow-master" -e GIT_BRANCH="$BRANCH_NAME" -e GIT_NAME="$GIT_NAME" -e GIT_TOKEN="$GITHUB_TOKEN" -e NPM_TOKEN="$NPM_TOKEN" eeacms/gitflow'''
+ sh '''docker run -i --rm --name="$BUILD_TAG-gitflow-master" -e GIT_BRANCH="$BRANCH_NAME" -e GIT_NAME="$GIT_NAME" -e GIT_TOKEN="$GITHUB_TOKEN" -e NPM_TOKEN="$NPM_TOKEN" -e LANGUAGE=javascript eeacms/gitflow'''
}
}
}
@@ -136,24 +188,21 @@ pipeline {
}
post {
+ always {
+ cleanWs(cleanWhenAborted: true, cleanWhenFailure: true, cleanWhenNotBuilt: true, cleanWhenSuccess: true, cleanWhenUnstable: true, deleteDirs: true)
+ }
changed {
script {
- def url = "${env.BUILD_URL}/display/redirect"
- def status = currentBuild.currentResult
- def subject = "${status}: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]'"
- def summary = "${subject} (${url})"
- def details = """
${env.JOB_NAME} - Build #${env.BUILD_NUMBER} - ${status}
- Check console output at ${env.JOB_BASE_NAME} - #${env.BUILD_NUMBER}
+ def details = """${env.JOB_NAME} - Build #${env.BUILD_NUMBER} - ${currentBuild.currentResult}
+ Check console output at ${env.JOB_BASE_NAME} - #${env.BUILD_NUMBER}
"""
-
- def color = '#FFFF00'
- if (status == 'SUCCESS') {
- color = '#00FF00'
- } else if (status == 'FAILURE') {
- color = '#FF0000'
- }
-
- emailext (subject: '$DEFAULT_SUBJECT', to: '$DEFAULT_RECIPIENTS', body: details)
+ emailext(
+ subject: '$DEFAULT_SUBJECT',
+ body: details,
+ attachLog: true,
+ compressLog: true,
+ recipientProviders: [[$class: 'DevelopersRecipientProvider'], [$class: 'CulpritsRecipientProvider']]
+ )
}
}
}
diff --git a/bootstrap b/bootstrap
index 8b2c40f..8613750 100644
--- a/bootstrap
+++ b/bootstrap
@@ -1,5 +1,6 @@
const path = require('path');
const fs = require('fs');
+const ejs = require('ejs');
const currentDir = path.basename(process.cwd());
@@ -8,13 +9,23 @@ const bootstrap = function (ofile) {
if (err) {
return console.log(err);
}
- var result = data.replace(/volto-addon-template/g, currentDir);
-
- fs.writeFile(ofile, result, 'utf8', function (err) {
+ const result = ejs.render(data, {
+ addonName: `@eeacms/${currentDir}`,
+ name: currentDir
+ });
+ const output = ofile.replace('.tpl', '');
+ fs.writeFile(output, result, 'utf8', function (err) {
if (err) {
return console.log(err);
}
});
+ if (ofile.includes('.tpl')) {
+ fs.unlink(ofile, (err) => {
+ if (err) {
+ return console.error(err);
+ }
+ });
+ }
});
}
diff --git a/cypress.json b/cypress.json
index 23eedae..aef675e 100644
--- a/cypress.json
+++ b/cypress.json
@@ -6,6 +6,7 @@
"video": true,
"reporterOptions": {
"mochaFile": "cypress/reports/cypress-[hash].xml",
+ "jenkinsMode": true,
"toConsole": true
}
}
diff --git a/cypress/fixtures/example.json b/cypress/fixtures/example.json
new file mode 100644
index 0000000..da18d93
--- /dev/null
+++ b/cypress/fixtures/example.json
@@ -0,0 +1,5 @@
+{
+ "name": "Using fixtures to represent data",
+ "email": "hello@cypress.io",
+ "body": "Fixtures are a great way to mock data for responses to routes"
+}
\ No newline at end of file
diff --git a/cypress/integration/block-basics.js b/cypress/integration/block-basics.js
new file mode 100644
index 0000000..454084c
--- /dev/null
+++ b/cypress/integration/block-basics.js
@@ -0,0 +1,32 @@
+import { setupBeforeEach, tearDownAfterEach } from '../support';
+
+describe('Blocks Tests', () => {
+ beforeEach(setupBeforeEach);
+ afterEach(tearDownAfterEach);
+
+ it('Add Block: Empty', () => {
+ // Change page title
+ cy.get('.documentFirstHeading > .public-DraftStyleDefault-block')
+ .clear()
+ .type('My Add-on Page')
+ .get('.documentFirstHeading span[data-text]')
+ .contains('My Add-on Page');
+
+ cy.get('.documentFirstHeading > .public-DraftStyleDefault-block').type(
+ '{enter}',
+ );
+
+ // Add block
+ cy.get('.ui.basic.icon.button.block-add-button').first().click();
+ 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/plugins/index.js b/cypress/plugins/index.js
new file mode 100644
index 0000000..27a31a5
--- /dev/null
+++ b/cypress/plugins/index.js
@@ -0,0 +1,26 @@
+///
+// ***********************************************************
+// This example plugins/index.js can be used to load plugins
+//
+// You can change the location of this file or turn off loading
+// the plugins file with the 'pluginsFile' configuration option.
+//
+// You can read more here:
+// https://on.cypress.io/plugins-guide
+// ***********************************************************
+
+// This function is called when a project is opened or re-opened (e.g. due to
+// the project's config changing)
+
+/**
+ * @type {Cypress.PluginConfig}
+ */
+module.exports = (on, config) => {
+ // `on` is used to hook into various events Cypress emits
+ // `config` is the resolved Cypress config
+ /* coverage-start
+ require('@cypress/code-coverage/task')(on, config)
+ on('file:preprocessor', require('@cypress/code-coverage/use-babelrc'))
+ return config
+ coverage-end */
+};
diff --git a/cypress/support/commands.js b/cypress/support/commands.js
new file mode 100644
index 0000000..ac48461
--- /dev/null
+++ b/cypress/support/commands.js
@@ -0,0 +1,315 @@
+/* eslint no-console: ["error", { allow: ["log"] }] */
+
+// --- 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': 'text' },
+ },
+ 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`));
+ }
+ },
+);
+
+// --- 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`));
+});
+
+// --- 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').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);
+ }
+ });
+ },
+);
+
+// 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');
+});
diff --git a/cypress/support/index.js b/cypress/support/index.js
new file mode 100644
index 0000000..a3fd935
--- /dev/null
+++ b/cypress/support/index.js
@@ -0,0 +1,53 @@
+// ***********************************************************
+// 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';
+
+// Alternatively you can use CommonJS syntax:
+// require('./commands')
+
+/* coverage-start
+//Generate code-coverage
+import '@cypress/code-coverage/support';
+coverage-end */
+
+export const setupBeforeEach = () => {
+ cy.autologin();
+ cy.createContent({
+ contentType: 'Folder',
+ contentId: 'cypress',
+ contentTitle: 'Cypress',
+ });
+ cy.createContent({
+ contentType: 'Document',
+ contentId: 'my-page',
+ contentTitle: 'My Page',
+ path: 'cypress',
+ });
+ cy.visit('/cypress/my-page');
+ cy.waitForResourceToLoad('@navigation');
+ cy.waitForResourceToLoad('@breadcrumbs');
+ cy.waitForResourceToLoad('@actions');
+ cy.waitForResourceToLoad('@types');
+ cy.waitForResourceToLoad('my-page');
+ cy.navigate('/cypress/my-page/edit');
+ cy.get(`.block.title [data-contents]`);
+};
+
+export const tearDownAfterEach = () => {
+ cy.autologin();
+ cy.removeContent('cypress');
+};
diff --git a/package.json b/package.json
index ecebeab..0a95b77 100644
--- a/package.json
+++ b/package.json
@@ -16,16 +16,23 @@
"type": "git",
"url": "git@github.com:eea/volto-widget-temporal-coverage.git"
},
- "dependencies": {},
+ "dependencies": {
+ },
+ "devDependencies": {
+ "@cypress/code-coverage": "^3.9.5",
+ "babel-plugin-transform-class-properties": "^6.24.1"
+ },
"scripts": {
"release": "release-it",
- "bootstrap": "node bootstrap",
+ "bootstrap": "npm install -g ejs; npm link ejs; node bootstrap",
"stylelint": "../../../node_modules/stylelint/bin/stylelint.js --allow-empty-input 'src/**/*.{css,less}'",
"stylelint:overrides": "../../../node_modules/.bin/stylelint --syntax less --allow-empty-input 'theme/**/*.overrides' 'src/**/*.overrides'",
"stylelint:fix": "yarn stylelint --fix && yarn stylelint:overrides --fix",
"prettier": "../../../node_modules/.bin/prettier --single-quote --check 'src/**/*.{js,jsx,json,css,less,md}'",
"prettier:fix": "../../../node_modules/.bin/prettier --single-quote --write 'src/**/*.{js,jsx,json,css,less,md}'",
"lint": "../../../node_modules/eslint/bin/eslint.js --max-warnings=0 'src/**/*.{js,jsx}'",
- "lint:fix": "../../../node_modules/eslint/bin/eslint.js --fix 'src/**/*.{js,jsx}'"
+ "lint:fix": "../../../node_modules/eslint/bin/eslint.js --fix 'src/**/*.{js,jsx}'",
+ "cypress:run": "../../../node_modules/cypress/bin/cypress run",
+ "cypress:open": "../../../node_modules/cypress/bin/cypress open"
}
}