diff --git a/.coverage.babel.config.js b/.coverage.babel.config.js new file mode 100644 index 0000000..37219fd --- /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('istanbul'); + return voltoBabel; +} + +module.exports = applyDefault; diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..53b9801 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +.vscode/ +.history +.eslintrc.js +.nyc_output +project +coverage +logs +*.log +npm-debug.log* +.DS_Store +*.swp +yarn-error.log +yarn.lock +package-lock.json + +node_modules +build +dist +cypress/videos +cypress/reports +screenshots +videos +.env.local +.env.development.local +.env.test.local +.env.production.local +*~ diff --git a/.i18n.babel.config.js b/.i18n.babel.config.js new file mode 100644 index 0000000..a900a75 --- /dev/null +++ b/.i18n.babel.config.js @@ -0,0 +1 @@ +module.exports = require('@plone/volto/babel'); diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..080322a --- /dev/null +++ b/.npmignore @@ -0,0 +1,96 @@ +# https://docs.npmjs.com/using-npm/developers.html#keeping-files-out-of-your-package + +# Directories +api/ +bin/ +build/ +lib/ +g-api/ +tests/ + +# Docs +docs/ + +# Cypress +cypress/ + +# Tests +__tests__/ +*.snap + +# Files +.travis.yml +requirements-docs.txt +requirements-tests.txt +yarn.lock +.dockerignore +.gitattributes +.yarnrc +.nvmrc +changelogupdater.js +pip-selfcheck.json +Dockerfile +CNAME +entrypoint.sh +Jenkinsfile +Makefile + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# next.js build output +.next + +styleguide.config +.vscode +packages diff --git a/.project.eslintrc.js b/.project.eslintrc.js new file mode 100644 index 0000000..765070f --- /dev/null +++ b/.project.eslintrc.js @@ -0,0 +1,48 @@ +const fs = require('fs'); +const path = require('path'); + +const projectRootPath = fs.existsSync('./project') + ? fs.realpathSync('./project') + : fs.realpathSync('./../../../'); +const packageJson = require(path.join(projectRootPath, 'package.json')); +const jsConfig = require(path.join(projectRootPath, 'jsconfig.json')).compilerOptions; + +const pathsConfig = jsConfig.paths; + +let voltoPath = path.join(projectRootPath, 'node_modules/@plone/volto'); + +Object.keys(pathsConfig).forEach(pkg => { + if (pkg === '@plone/volto') { + voltoPath = `./${jsConfig.baseUrl}/${pathsConfig[pkg][0]}`; + } +}); +const AddonConfigurationRegistry = require(`${voltoPath}/addon-registry.js`); +const reg = new AddonConfigurationRegistry(projectRootPath); + +// Extends ESlint configuration for adding the aliases to `src` directories in Volto addons +const addonAliases = Object.keys(reg.packages).map(o => [ + o, + reg.packages[o].modulePath, +]); + + +module.exports = { + extends: `${projectRootPath}/node_modules/@plone/volto/.eslintrc`, + settings: { + 'import/resolver': { + alias: { + map: [ + ['@plone/volto', '@plone/volto/src'], + ...addonAliases, + ['@package', `${__dirname}/src`], + ['~', `${__dirname}/src`], + ], + extensions: ['.js', '.jsx', '.json'], + }, + 'babel-plugin-root-import': { + rootPathSuffix: 'src', + }, + }, + }, +}; + diff --git a/.release-it.json b/.release-it.json new file mode 100644 index 0000000..a029900 --- /dev/null +++ b/.release-it.json @@ -0,0 +1,17 @@ +{ + "npm": { + "publish": false + }, + "git": { + "changelog": "npx auto-changelog --stdout --commit-limit false -u --template https://raw.githubusercontent.com/release-it/release-it/master/templates/changelog-compact.hbs", + "tagName": "${version}" + }, + "github": { + "release": true, + "releaseName": "${version}", + "releaseNotes": "npx auto-changelog --stdout --commit-limit false -u --template https://raw.githubusercontent.com/release-it/release-it/master/templates/changelog-compact.hbs" + }, + "hooks": { + "after:bump": "npx auto-changelog --commit-limit false -p" + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3742d31 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## 0.1.0 + +- Initial release diff --git a/DEVELOP.md b/DEVELOP.md new file mode 100644 index 0000000..9c01eb6 --- /dev/null +++ b/DEVELOP.md @@ -0,0 +1,51 @@ +# volto-addon-template + +## Develop + +Before starting make sure your development environment is properly set. See [Volto Developer Documentation](https://docs.voltocms.com/getting-started/install/) + +1. Make sure you have installed `yo`, `@plone/generator-volto` and `mrs-developer` + + npm install -g yo @plone/generator-volto mrs-developer + +1. Create new volto app + + yo @plone/volto my-volto-project --addon @eeacms/volto-addon-template --skip-install + cd my-volto-project + +1. Add the following to `mrs.developer.json`: + + { + "volto-addon-template": { + "url": "https://github.com/eea/volto-addon-template.git", + "package": "@eeacms/volto-addon-template", + "branch": "develop", + "path": "src" + } + } + +1. Install + + yarn develop + yarn + +1. Start backend + + docker pull plone + docker run -d --name plone -p 8080:8080 -e SITE=Plone -e PROFILES="profile-plone.restapi:blocks" plone + + ...wait for backend to setup and start - `Ready to handle requests`: + + docker logs -f plone + + ...you can also check http://localhost:8080/Plone + +1. Start frontend + + yarn start + +1. Go to http://localhost:3000 + +1. Happy hacking! + + cd src/addons/volto-addon-template/ diff --git a/DEVELOP.md.tpl b/DEVELOP.md.tpl new file mode 100644 index 0000000..cf7da06 --- /dev/null +++ b/DEVELOP.md.tpl @@ -0,0 +1,51 @@ +# <%= name %> + +## Develop + +Before starting make sure your development environment is properly set. See [Volto Developer Documentation](https://docs.voltocms.com/getting-started/install/) + +1. Make sure you have installed `yo`, `@plone/generator-volto` and `mrs-developer` + + npm install -g yo @plone/generator-volto mrs-developer + +1. Create new volto app + + yo @plone/volto my-volto-project --addon <%= addonName %> --skip-install + cd my-volto-project + +1. Add the following to `mrs.developer.json`: + + { + "<%= name %>": { + "url": "https://github.com/eea/<%= name %>.git", + "package": "<%= addonName %>", + "branch": "develop", + "path": "src" + } + } + +1. Install + + yarn develop + yarn + +1. Start backend + + docker pull plone + docker run -d --name plone -p 8080:8080 -e SITE=Plone -e PROFILES="profile-plone.restapi:blocks" plone + + ...wait for backend to setup and start - `Ready to handle requests`: + + docker logs -f plone + + ...you can also check http://localhost:8080/Plone + +1. Start frontend + + yarn start + +1. Go to http://localhost:3000 + +1. Happy hacking! + + cd src/addons/<%= name %>/ diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..051b751 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,242 @@ +pipeline { + agent any + + environment { + GIT_NAME = "volto-addon-template" + NAMESPACE = "@eeacms" + SONARQUBE_TAGS = "volto.eea.europa.eu" + DEPENDENCIES = "" + VOLTO = "alpha" + } + + stages { + + stage('Release') { + when { + allOf { + environment name: 'CHANGE_ID', value: '' + branch 'master' + } + } + steps { + 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" -e LANGUAGE=javascript eeacms/gitflow''' + } + } + } + } + + stage('Code') { + when { + allOf { + environment name: 'CHANGE_ID', value: '' + not { changelog '.*^Automated release [0-9\\.]+$' } + not { branch 'master' } + } + } + steps { + parallel( + + "ES lint": { + node(label: 'docker') { + sh '''docker run -i --rm --name="$BUILD_TAG-eslint" -e NAMESPACE="$NAMESPACE" -e GIT_NAME=$GIT_NAME -e GIT_BRANCH="$BRANCH_NAME" -e GIT_CHANGE_ID="$CHANGE_ID" -e VOLTO=$VOLTO plone/volto-addon-ci:alpha eslint''' + } + }, + + "Style lint": { + node(label: 'docker') { + sh '''docker run -i --rm --name="$BUILD_TAG-stylelint" -e NAMESPACE="$NAMESPACE" -e GIT_NAME=$GIT_NAME -e GIT_BRANCH="$BRANCH_NAME" -e GIT_CHANGE_ID="$CHANGE_ID" -e VOLTO=$VOLTO plone/volto-addon-ci:alpha stylelint''' + } + }, + + "Prettier": { + node(label: 'docker') { + sh '''docker run -i --rm --name="$BUILD_TAG-prettier" -e NAMESPACE="$NAMESPACE" -e GIT_NAME=$GIT_NAME -e GIT_BRANCH="$BRANCH_NAME" -e GIT_CHANGE_ID="$CHANGE_ID" -e VOLTO=$VOLTO plone/volto-addon-ci:alpha prettier''' + } + } + ) + } + } + + stage('Tests') { + when { + allOf { + environment name: 'CHANGE_ID', value: '' + anyOf { + not { changelog '.*^Automated release [0-9\\.]+$' } + branch 'master' + } + } + } + steps { + parallel( + + "Volto": { + node(label: 'docker') { + script { + try { + sh '''docker pull plone/volto-addon-ci:alpha''' + 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" -e VOLTO=$VOLTO plone/volto-addon-ci:alpha''' + 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/**" + archiveArtifacts artifacts: "xunit-reports/unit_tests_log.txt", fingerprint: true + publishHTML (target : [ + allowMissing: false, + alwaysLinkToLastBuild: true, + keepAll: true, + reportDir: 'xunit-reports/coverage/lcov-report', + reportFiles: 'index.html', + reportName: 'UTCoverage', + reportTitles: 'Unit Tests Code Coverage' + ]) + } finally { + catchError(buildResult: 'SUCCESS', stageResult: 'SUCCESS') { + junit testResults: 'xunit-reports/junit.xml', allowEmptyResults: true + } + sh script: '''docker rm -v $BUILD_TAG-volto''', returnStatus: true + } + } + } + } + ) + } + } + + stage('Integration tests') { + when { + allOf { + environment name: 'CHANGE_ID', value: '' + anyOf { + not { changelog '.*^Automated release [0-9\\.]+$' } + branch 'master' + } + } + } + steps { + parallel( + + "Cypress": { + node(label: 'docker') { + script { + try { + 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 plone/volto-addon-ci:alpha; 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" -e NODE_ENV=development -e VOLTO=$VOLTO plone/volto-addon-ci:alpha 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']) + } + sh '''touch empty_file; for ok_test in $(grep -E 'file=.*failures="0"' $(grep 'testsuites .*failures="0"' $(find cypress-results -name *.xml) empty_file | awk -F: '{print $1}') empty_file | sed 's/.* file="\\(.*\\)" time.*/\\1/' | sed 's#^cypress/integration/##g' | sed 's#^../../../node_modules/@eeacms/##g'); do rm -f cypress-reports/videos/$ok_test.mp4; rm -f cypress-reports/$ok_test.mp4; done''' + archiveArtifacts artifacts: 'cypress-reports/**/*.mp4', fingerprint: true, allowEmptyArchive: 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') { + when { + allOf { + environment name: 'CHANGE_ID', value: '' + anyOf { + branch 'master' + allOf { + branch 'develop' + not { changelog '.*^Automated release [0-9\\.]+$' } + } + } + } + } + steps { + node(label: 'swarm') { + script{ + checkout scm + unstash "xunit-reports" + unstash "cypress-coverage" + def scannerHome = tool 'SonarQubeScanner'; + def nodeJS = tool 'NodeJS'; + withSonarQubeEnv('Sonarqube') { + sh '''sed -i "s#/opt/frontend/my-volto-project/src/addons/${GIT_NAME}/##g" xunit-reports/coverage/lcov.info''' + sh "export PATH=${scannerHome}/bin:${nodeJS}/bin:$PATH; 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''' + } + } + } + } + } + + stage('Pull Request') { + when { + not { + environment name: 'CHANGE_ID', value: '' + } + environment name: 'CHANGE_TARGET', value: 'master' + } + steps { + node(label: 'docker') { + script { + if ( env.CHANGE_BRANCH != "develop" ) { + error "Pipeline aborted due to PR not made from develop branch" + } + 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" -e LANGUAGE=javascript eeacms/gitflow''' + } + } + } + } + } + + } + + post { + always { + cleanWs(cleanWhenAborted: true, cleanWhenFailure: true, cleanWhenNotBuilt: true, cleanWhenSuccess: true, cleanWhenUnstable: true, deleteDirs: true) + } + changed { + script { + def details = """
Check console output at ${env.JOB_BASE_NAME} - #${env.BUILD_NUMBER}
+ """ + emailext( + subject: '$DEFAULT_SUBJECT', + body: details, + attachLog: true, + compressLog: true, + recipientProviders: [[$class: 'DevelopersRecipientProvider'], [$class: 'CulpritsRecipientProvider']] + ) + } + } + } +} diff --git a/Jenkinsfile.tpl b/Jenkinsfile.tpl new file mode 100644 index 0000000..6de295e --- /dev/null +++ b/Jenkinsfile.tpl @@ -0,0 +1,242 @@ +pipeline { + agent any + + environment { + GIT_NAME = "<%= name %>" + NAMESPACE = "@eeacms" + SONARQUBE_TAGS = "volto.eea.europa.eu" + DEPENDENCIES = "" + VOLTO = "alpha" + } + + stages { + + stage('Release') { + when { + allOf { + environment name: 'CHANGE_ID', value: '' + branch 'master' + } + } + steps { + 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" -e LANGUAGE=javascript eeacms/gitflow''' + } + } + } + } + + stage('Code') { + when { + allOf { + environment name: 'CHANGE_ID', value: '' + not { changelog '.*^Automated release [0-9\\.]+$' } + not { branch 'master' } + } + } + steps { + parallel( + + "ES lint": { + node(label: 'docker') { + sh '''docker run -i --rm --name="$BUILD_TAG-eslint" -e NAMESPACE="$NAMESPACE" -e GIT_NAME=$GIT_NAME -e GIT_BRANCH="$BRANCH_NAME" -e GIT_CHANGE_ID="$CHANGE_ID" -e VOLTO=$VOLTO plone/volto-addon-ci:alpha eslint''' + } + }, + + "Style lint": { + node(label: 'docker') { + sh '''docker run -i --rm --name="$BUILD_TAG-stylelint" -e NAMESPACE="$NAMESPACE" -e GIT_NAME=$GIT_NAME -e GIT_BRANCH="$BRANCH_NAME" -e GIT_CHANGE_ID="$CHANGE_ID" -e VOLTO=$VOLTO plone/volto-addon-ci:alpha stylelint''' + } + }, + + "Prettier": { + node(label: 'docker') { + sh '''docker run -i --rm --name="$BUILD_TAG-prettier" -e NAMESPACE="$NAMESPACE" -e GIT_NAME=$GIT_NAME -e GIT_BRANCH="$BRANCH_NAME" -e GIT_CHANGE_ID="$CHANGE_ID" -e VOLTO=$VOLTO plone/volto-addon-ci:alpha prettier''' + } + } + ) + } + } + + stage('Tests') { + when { + allOf { + environment name: 'CHANGE_ID', value: '' + anyOf { + not { changelog '.*^Automated release [0-9\\.]+$' } + branch 'master' + } + } + } + steps { + parallel( + + "Volto": { + node(label: 'docker') { + script { + try { + sh '''docker pull plone/volto-addon-ci:alpha''' + 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" -e VOLTO=$VOLTO plone/volto-addon-ci:alpha''' + 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/**" + archiveArtifacts artifacts: "xunit-reports/unit_tests_log.txt", fingerprint: true + publishHTML (target : [ + allowMissing: false, + alwaysLinkToLastBuild: true, + keepAll: true, + reportDir: 'xunit-reports/coverage/lcov-report', + reportFiles: 'index.html', + reportName: 'UTCoverage', + reportTitles: 'Unit Tests Code Coverage' + ]) + } finally { + catchError(buildResult: 'SUCCESS', stageResult: 'SUCCESS') { + junit testResults: 'xunit-reports/junit.xml', allowEmptyResults: true + } + sh script: '''docker rm -v $BUILD_TAG-volto''', returnStatus: true + } + } + } + } + ) + } + } + + stage('Integration tests') { + when { + allOf { + environment name: 'CHANGE_ID', value: '' + anyOf { + not { changelog '.*^Automated release [0-9\\.]+$' } + branch 'master' + } + } + } + steps { + parallel( + + "Cypress": { + node(label: 'docker') { + script { + try { + 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 plone/volto-addon-ci:alpha; 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" -e VOLTO=$VOLTO plone/volto-addon-ci:alpha 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']) + } + sh '''touch empty_file; for ok_test in $(grep -E 'file=.*failures="0"' $(grep 'testsuites .*failures="0"' $(find cypress-results -name *.xml) empty_file | awk -F: '{print $1}') empty_file | sed 's/.* file="\\(.*\\)" time.*/\\1/' | sed 's#^cypress/integration/##g' | sed 's#^../../../node_modules/@eeacms/##g'); do rm -f cypress-reports/videos/$ok_test.mp4; rm -f cypress-reports/$ok_test.mp4; done''' + archiveArtifacts artifacts: 'cypress-reports/**/*.mp4', fingerprint: true, allowEmptyArchive: 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') { + when { + allOf { + environment name: 'CHANGE_ID', value: '' + anyOf { + branch 'master' + allOf { + branch 'develop' + not { changelog '.*^Automated release [0-9\\.]+$' } + } + } + } + } + steps { + node(label: 'swarm') { + script{ + checkout scm + unstash "xunit-reports" + unstash "cypress-coverage" + def scannerHome = tool 'SonarQubeScanner'; + def nodeJS = tool 'NodeJS'; + withSonarQubeEnv('Sonarqube') { + sh '''sed -i "s#/opt/frontend/my-volto-project/src/addons/${GIT_NAME}/##g" xunit-reports/coverage/lcov.info''' + sh "export PATH=${scannerHome}/bin:${nodeJS}/bin:$PATH; 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''' + } + } + } + } + } + + stage('Pull Request') { + when { + not { + environment name: 'CHANGE_ID', value: '' + } + environment name: 'CHANGE_TARGET', value: 'master' + } + steps { + node(label: 'docker') { + script { + if ( env.CHANGE_BRANCH != "develop" ) { + error "Pipeline aborted due to PR not made from develop branch" + } + 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" -e LANGUAGE=javascript eeacms/gitflow''' + } + } + } + } + } + + } + + post { + always { + cleanWs(cleanWhenAborted: true, cleanWhenFailure: true, cleanWhenNotBuilt: true, cleanWhenSuccess: true, cleanWhenUnstable: true, deleteDirs: true) + } + changed { + script { + def details = """Check console output at ${env.JOB_BASE_NAME} - #${env.BUILD_NUMBER}
+ """ + emailext( + subject: '$DEFAULT_SUBJECT', + body: details, + attachLog: true, + compressLog: true, + recipientProviders: [[$class: 'DevelopersRecipientProvider'], [$class: 'CulpritsRecipientProvider']] + ) + } + } + } +} diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..0992c00 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2020 European Environment Agency + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a11bb77 --- /dev/null +++ b/Makefile @@ -0,0 +1,97 @@ +SHELL=/bin/bash + +DIR=$(shell basename $$(pwd)) +ADDON ?= "@eeacms/volto-addon-template" + +# We like colors +# From: https://coderwall.com/p/izxssa/colored-makefile-for-golang-projects +RED=`tput setaf 1` +GREEN=`tput setaf 2` +RESET=`tput sgr0` +YELLOW=`tput setaf 3` + +ifeq ($(wildcard ./project),) + NODE_MODULES = "../../../node_modules" +else + NODE_MODULES = "./project/node_modules" +endif + +project: + npm install -g yo + npm install -g @plone/generator-volto + npm install -g mrs-developer + yo @plone/volto project --addon ${ADDON} --workspace "src/addons/${DIR}" --no-interactive + ln -sf $$(pwd) project/src/addons/ + cp .project.eslintrc.js .eslintrc.js + cd project && yarn + @echo "-------------------" + @echo "$(GREEN)Volto project is ready!$(RESET)" + @echo "$(RED)Now run: cd project && yarn start$(RESET)" + +all: project + +.PHONY: start-test-backend +start-test-backend: ## Start Test Plone Backend + @echo "$(GREEN)==> Start Test Plone Backend$(RESET)" + docker run -i --rm -e ZSERVER_HOST=0.0.0.0 -e ZSERVER_PORT=55001 -p 55001:55001 -e SITE=plone -e APPLY_PROFILES=plone.app.contenttypes:plone-content,plone.restapi:default,kitconcept.volto:default-homepage -e CONFIGURE_PACKAGES=plone.app.contenttypes,plone.restapi,kitconcept.volto,kitconcept.volto.cors -e ADDONS='plone.app.robotframework plone.app.contenttypes plone.restapi kitconcept.volto' plone ./bin/robot-server plone.app.robotframework.testing.PLONE_ROBOT_TESTING + +.PHONY: start-backend-docker +start-backend-docker: ## Starts a Docker-based backend + @echo "$(GREEN)==> Start Docker-based Plone Backend$(RESET)" + docker run -it --rm --name=plone -p 8080:8080 -e SITE=Plone -e ADDONS="kitconcept.volto" -e ZCML="kitconcept.volto.cors" plone + +.PHONY: test +test: ## Run jest tests + docker pull plone/volto-addon-ci:alpha + docker run -it --rm -e NAMESPACE="@eeacms" -e GIT_NAME="${DIR}" -e RAZZLE_JEST_CONFIG=jest-addon.config.js -v "$$(pwd):/opt/frontend/my-volto-project/src/addons/${DIR}" -e CI="true" plone/volto-addon-ci:alpha + +.PHONY: test-update +test-update: ## Update jest tests snapshots + docker pull plone/volto-addon-ci:alpha + docker run -it --rm -e NAMESPACE="@eeacms" -e GIT_NAME="${DIR}" -e RAZZLE_JEST_CONFIG=jest-addon.config.js -v "$$(pwd):/opt/frontend/my-volto-project/src/addons/${DIR}" -e CI="true" plone/volto-addon-ci:alpha yarn test src/addons/${DIR}/src --watchAll=false -u + +.PHONY: stylelint +stylelint: ## Stylelint + $(NODE_MODULES)/stylelint/bin/stylelint.js --allow-empty-input 'src/**/*.{css,less}' + +.PHONY: stylelint-overrides +stylelint-overrides: + $(NODE_MODULES)/.bin/stylelint --syntax less --allow-empty-input 'theme/**/*.overrides' 'src/**/*.overrides' + +.PHONY: stylelint-fix +stylelint-fix: ## Fix stylelint + $(NODE_MODULES)/stylelint/bin/stylelint.js --allow-empty-input 'src/**/*.{css,less}' --fix + $(NODE_MODULES)/.bin/stylelint --syntax less --allow-empty-input 'theme/**/*.overrides' 'src/**/*.overrides' --fix + +.PHONY: prettier +prettier: ## Prettier + $(NODE_MODULES)/.bin/prettier --single-quote --check 'src/**/*.{js,jsx,json,css,less,md}' + +.PHONY: prettier-fix +prettier-fix: ## Fix prettier + $(NODE_MODULES)/.bin/prettier --single-quote --write 'src/**/*.{js,jsx,json,css,less,md}' + +.PHONY: lint +lint: ## ES Lint + $(NODE_MODULES)/eslint/bin/eslint.js --max-warnings=0 'src/**/*.{js,jsx}' + +.PHONY: lint-fix +lint-fix: ## Fix ES Lint + $(NODE_MODULES)/eslint/bin/eslint.js --fix 'src/**/*.{js,jsx}' + +.PHONY: i18n +i18n: ## i18n + rm -rf build/messages + NODE_ENV=development $(NODE_MODULES)/.bin/i18n --addon + +.PHONY: cypress-run +cypress-run: ## Run cypress integration tests + NODE_ENV=development $(NODE_MODULES)/cypress/bin/cypress run + +.PHONY: cypress-open +cypress-open: ## Open cypress integration tests + NODE_ENV=development $(NODE_MODULES)/cypress/bin/cypress open + +.PHONY: help +help: ## Show this help. + @echo -e "$$(grep -hE '^\S+:.*##' $(MAKEFILE_LIST) | sed -e 's/:.*##\s*/:/' -e 's/^\(.\+\):\(.*\)/\\x1b[36m\1\\x1b[m:\2/' | column -c2 -t -s :)" diff --git a/Makefile.tpl b/Makefile.tpl new file mode 100644 index 0000000..b256914 --- /dev/null +++ b/Makefile.tpl @@ -0,0 +1,97 @@ +SHELL=/bin/bash + +DIR=$(shell basename $$(pwd)) +ADDON ?= "<%= addonName %>" + +# We like colors +# From: https://coderwall.com/p/izxssa/colored-makefile-for-golang-projects +RED=`tput setaf 1` +GREEN=`tput setaf 2` +RESET=`tput sgr0` +YELLOW=`tput setaf 3` + +ifeq ($(wildcard ./project),) + NODE_MODULES = "../../../node_modules" +else + NODE_MODULES = "./project/node_modules" +endif + +project: + npm install -g yo + npm install -g @plone/generator-volto + npm install -g mrs-developer + yo @plone/volto project --addon ${ADDON} --workspace "src/addons/${DIR}" --no-interactive + ln -sf $$(pwd) project/src/addons/ + cp .project.eslintrc.js .eslintrc.js + cd project && yarn + @echo "-------------------" + @echo "$(GREEN)Volto project is ready!$(RESET)" + @echo "$(RED)Now run: cd project && yarn start$(RESET)" + +all: project + +.PHONY: start-test-backend +start-test-backend: ## Start Test Plone Backend + @echo "$(GREEN)==> Start Test Plone Backend$(RESET)" + docker run -i --rm -e ZSERVER_HOST=0.0.0.0 -e ZSERVER_PORT=55001 -p 55001:55001 -e SITE=plone -e APPLY_PROFILES=plone.app.contenttypes:plone-content,plone.restapi:default,kitconcept.volto:default-homepage -e CONFIGURE_PACKAGES=plone.app.contenttypes,plone.restapi,kitconcept.volto,kitconcept.volto.cors -e ADDONS='plone.app.robotframework plone.app.contenttypes plone.restapi kitconcept.volto' plone ./bin/robot-server plone.app.robotframework.testing.PLONE_ROBOT_TESTING + +.PHONY: start-backend-docker +start-backend-docker: ## Starts a Docker-based backend + @echo "$(GREEN)==> Start Docker-based Plone Backend$(RESET)" + docker run -it --rm --name=plone -p 8080:8080 -e SITE=Plone -e ADDONS="kitconcept.volto" -e ZCML="kitconcept.volto.cors" plone + +.PHONY: test +test: ## Run jest tests + docker pull plone/volto-addon-ci:alpha + docker run -it --rm -e NAMESPACE="@eeacms" -e GIT_NAME="${DIR}" -e RAZZLE_JEST_CONFIG=jest-addon.config.js -v "$$(pwd):/opt/frontend/my-volto-project/src/addons/${DIR}" -e CI="true" plone/volto-addon-ci:alpha + +.PHONY: test-update +test-update: ## Update jest tests snapshots + docker pull plone/volto-addon-ci:alpha + docker run -it --rm -e NAMESPACE="@eeacms" -e GIT_NAME="${DIR}" -e RAZZLE_JEST_CONFIG=jest-addon.config.js -v "$$(pwd):/opt/frontend/my-volto-project/src/addons/${DIR}" -e CI="true" plone/volto-addon-ci:alpha yarn test src/addons/${DIR}/src --watchAll=false -u + +.PHONY: stylelint +stylelint: ## Stylelint + $(NODE_MODULES)/stylelint/bin/stylelint.js --allow-empty-input 'src/**/*.{css,less}' + +.PHONY: stylelint-overrides +stylelint-overrides: + $(NODE_MODULES)/.bin/stylelint --syntax less --allow-empty-input 'theme/**/*.overrides' 'src/**/*.overrides' + +.PHONY: stylelint-fix +stylelint-fix: ## Fix stylelint + $(NODE_MODULES)/stylelint/bin/stylelint.js --allow-empty-input 'src/**/*.{css,less}' --fix + $(NODE_MODULES)/.bin/stylelint --syntax less --allow-empty-input 'theme/**/*.overrides' 'src/**/*.overrides' --fix + +.PHONY: prettier +prettier: ## Prettier + $(NODE_MODULES)/.bin/prettier --single-quote --check 'src/**/*.{js,jsx,json,css,less,md}' + +.PHONY: prettier-fix +prettier-fix: ## Fix prettier + $(NODE_MODULES)/.bin/prettier --single-quote --write 'src/**/*.{js,jsx,json,css,less,md}' + +.PHONY: lint +lint: ## ES Lint + $(NODE_MODULES)/eslint/bin/eslint.js --max-warnings=0 'src/**/*.{js,jsx}' + +.PHONY: lint-fix +lint-fix: ## Fix ES Lint + $(NODE_MODULES)/eslint/bin/eslint.js --fix 'src/**/*.{js,jsx}' + +.PHONY: i18n +i18n: ## i18n + rm -rf build/messages + NODE_ENV=development $(NODE_MODULES)/.bin/i18n --addon + +.PHONY: cypress-run +cypress-run: ## Run cypress integration tests + NODE_ENV=development $(NODE_MODULES)/cypress/bin/cypress run + +.PHONY: cypress-open +cypress-open: ## Open cypress integration tests + NODE_ENV=development $(NODE_MODULES)/cypress/bin/cypress open + +.PHONY: help +help: ## Show this help. + @echo -e "$$(grep -hE '^\S+:.*##' $(MAKEFILE_LIST) | sed -e 's/:.*##\s*/:/' -e 's/^\(.\+\):\(.*\)/\\x1b[36m\1\\x1b[m:\2/' | column -c2 -t -s :)" diff --git a/README.md b/README.md new file mode 100644 index 0000000..5cc8add --- /dev/null +++ b/README.md @@ -0,0 +1,80 @@ +# volto-addon-template + +[![Releases](https://img.shields.io/github/v/release/eea/volto-addon-template)](https://github.com/eea/volto-addon-template/releases) + +[![Pipeline](https://ci.eionet.europa.eu/buildStatus/icon?job=volto-addons%2Fvolto-addon-template%2Fmaster&subject=master)](https://ci.eionet.europa.eu/view/Github/job/volto-addons/job/volto-addon-template/job/master/display/redirect) +[![Lines of Code](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-addon-template-master&metric=ncloc)](https://sonarqube.eea.europa.eu/dashboard?id=volto-addon-template-master) +[![Coverage](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-addon-template-master&metric=coverage)](https://sonarqube.eea.europa.eu/dashboard?id=volto-addon-template-master) +[![Bugs](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-addon-template-master&metric=bugs)](https://sonarqube.eea.europa.eu/dashboard?id=volto-addon-template-master) +[![Duplicated Lines (%)](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-addon-template-master&metric=duplicated_lines_density)](https://sonarqube.eea.europa.eu/dashboard?id=volto-addon-template-master) + +[![Pipeline](https://ci.eionet.europa.eu/buildStatus/icon?job=volto-addons%2Fvolto-addon-template%2Fdevelop&subject=develop)](https://ci.eionet.europa.eu/view/Github/job/volto-addons/job/volto-addon-template/job/develop/display/redirect) +[![Lines of Code](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-addon-template-develop&metric=ncloc)](https://sonarqube.eea.europa.eu/dashboard?id=volto-addon-template-develop) +[![Coverage](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-addon-template-develop&metric=coverage)](https://sonarqube.eea.europa.eu/dashboard?id=volto-addon-template-develop) +[![Bugs](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-addon-template-develop&metric=bugs)](https://sonarqube.eea.europa.eu/dashboard?id=volto-addon-template-develop) +[![Duplicated Lines (%)](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-addon-template-develop&metric=duplicated_lines_density)](https://sonarqube.eea.europa.eu/dashboard?id=volto-addon-template-develop) + + +[Volto](https://github.com/plone/volto) add-on + +## Features + +Demo GIF + +## Getting started + +### Add volto-addon-template to your Volto project + +1. Make sure you have a [Plone backend](https://plone.org/download) up-and-running at http://localhost:8080/Plone + +1. Start Volto frontend + +* If you already have a volto project, just update `package.json`: + + ```JSON + "addons": [ + "@eeacms/volto-addon-template" + ], + + "dependencies": { + "@eeacms/volto-addon-template": "*" + } + ``` + +* If not, create one: + + ``` + npm install -g yo @plone/generator-volto + yo @plone/volto my-volto-project --canary --addon @eeacms/volto-addon-template + cd my-volto-project + ``` + +1. Install new add-ons and restart Volto: + + ``` + yarn + yarn start + ``` + +1. Go to http://localhost:3000 + +1. Happy editing! + +## Release + +See [RELEASE.md](https://github.com/eea/volto-addon-template/blob/master/RELEASE.md). + +## How to contribute + +See [DEVELOP.md](https://github.com/eea/volto-addon-template/blob/master/DEVELOP.md). + +## Copyright and license + +The Initial Owner of the Original Code is European Environment Agency (EEA). +All Rights Reserved. + +See [LICENSE.md](https://github.com/eea/volto-addon-template/blob/master/LICENSE.md) for details. + +## Funding + +[European Environment Agency (EU)](http://eea.europa.eu) diff --git a/README.md.tpl b/README.md.tpl new file mode 100644 index 0000000..12e4b8f --- /dev/null +++ b/README.md.tpl @@ -0,0 +1,80 @@ +# <%= name %> + +[![Releases](https://img.shields.io/github/v/release/eea/<%= name %>)](https://github.com/eea/<%= name %>/releases) + +[![Pipeline](https://ci.eionet.europa.eu/buildStatus/icon?job=volto-addons%2F<%= name %>%2Fmaster&subject=master)](https://ci.eionet.europa.eu/view/Github/job/volto-addons/job/<%= name %>/job/master/display/redirect) +[![Lines of Code](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=<%= name %>-master&metric=ncloc)](https://sonarqube.eea.europa.eu/dashboard?id=<%= name %>-master) +[![Coverage](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=<%= name %>-master&metric=coverage)](https://sonarqube.eea.europa.eu/dashboard?id=<%= name %>-master) +[![Bugs](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=<%= name %>-master&metric=bugs)](https://sonarqube.eea.europa.eu/dashboard?id=<%= name %>-master) +[![Duplicated Lines (%)](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=<%= name %>-master&metric=duplicated_lines_density)](https://sonarqube.eea.europa.eu/dashboard?id=<%= name %>-master) + +[![Pipeline](https://ci.eionet.europa.eu/buildStatus/icon?job=volto-addons%2F<%= name %>%2Fdevelop&subject=develop)](https://ci.eionet.europa.eu/view/Github/job/volto-addons/job/<%= name %>/job/develop/display/redirect) +[![Lines of Code](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=<%= name %>-develop&metric=ncloc)](https://sonarqube.eea.europa.eu/dashboard?id=<%= name %>-develop) +[![Coverage](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=<%= name %>-develop&metric=coverage)](https://sonarqube.eea.europa.eu/dashboard?id=<%= name %>-develop) +[![Bugs](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=<%= name %>-develop&metric=bugs)](https://sonarqube.eea.europa.eu/dashboard?id=<%= name %>-develop) +[![Duplicated Lines (%)](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=<%= name %>-develop&metric=duplicated_lines_density)](https://sonarqube.eea.europa.eu/dashboard?id=<%= name %>-develop) + + +[Volto](https://github.com/plone/volto) add-on + +## Features + +Demo GIF + +## Getting started + +### Add <%= name %> to your Volto project + +1. Make sure you have a [Plone backend](https://plone.org/download) up-and-running at http://localhost:8080/Plone + +1. Start Volto frontend + +* If you already have a volto project, just update `package.json`: + + ```JSON + "addons": [ + "<%= addonName %>" + ], + + "dependencies": { + "<%= addonName %>": "*" + } + ``` + +* If not, create one: + + ``` + npm install -g yo @plone/generator-volto + yo @plone/volto my-volto-project --canary --addon <%= addonName %> + cd my-volto-project + ``` + +1. Install new add-ons and restart Volto: + + ``` + yarn + yarn start + ``` + +1. Go to http://localhost:3000 + +1. Happy editing! + +## Release + +See [RELEASE.md](https://github.com/eea/<%= name %>/blob/master/RELEASE.md). + +## How to contribute + +See [DEVELOP.md](https://github.com/eea/<%= name %>/blob/master/DEVELOP.md). + +## Copyright and license + +The Initial Owner of the Original Code is European Environment Agency (EEA). +All Rights Reserved. + +See [LICENSE.md](https://github.com/eea/<%= name %>/blob/master/LICENSE.md) for details. + +## Funding + +[European Environment Agency (EU)](http://eea.europa.eu) diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..24f98ee --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,74 @@ +## Release + +### Automatic release using Jenkins + +* The automatic release is started by creating a [Pull Request](../../compare/master...develop) from `develop` to `master`. The pull request status checks correlated to the branch and PR Jenkins jobs need to be processed successfully. 1 review from a github user with rights is mandatory. +* It runs on every commit on `master` branch, which is protected from direct commits, only allowing pull request merge commits. +* The automatic release is done by [Jenkins](https://ci.eionet.europa.eu). The status of the release job can be seen both in the Readme.md badges and the green check/red cross/yellow circle near the last commit information. If you click on the icon, you will have the list of checks that were run. The `continuous-integration/jenkins/branch` link goes to the Jenkins job execution webpage. +* Automated release scripts are located in the `eeacms/gitflow` docker image, specifically [js-release.sh](https://github.com/eea/eea.docker.gitflow/blob/master/src/js-release.sh) script. It uses the `release-it` tool. +* As long as a PR request is open from develop to master, the PR Jenkins job will automatically re-create the CHANGELOG.md and package.json files to be production-ready. +* The version format must be MAJOR.MINOR.PATCH. By default, next release is set to next minor version (with patch 0). +* You can manually change the version in `package.json`. The new version must not be already present in the tags/releases of the repository, otherwise it will be automatically increased by the script. Any changes to the version will trigger a `CHANGELOG.md` re-generation. +* Automated commits and commits with [JENKINS] or [YARN] in the commit log are excluded from `CHANGELOG.md` file. + +### Manual release from the develop branch ( beta release ) + +#### Installation and configuration of release-it + +You need to first install the [release-it](https://github.com/release-it/release-it) client. + + ``` + npm install -g release-it + ``` + +Release-it uses the configuration written in the [`.release-it.json`](./.release-it.json) file located in the root of the repository. + +Release-it is a tool that automates 4 important steps in the release process: + +1. Version increase in `package.json` ( increased from the current version in `package.json`) +2. `CHANGELOG.md` automatic generation from commit messages ( grouped by releases ) +3. GitHub release on the commit with the changelog and package.json modification on the develop branch +4. NPM release ( by default it's disabled, but can be enabled in the configuration file ) + +To configure the authentification, you need to export GITHUB_TOKEN for [GitHub](https://github.com/settings/tokens) + + ``` + export GITHUB_TOKEN=XXX-XXXXXXXXXXXXXXXXXXXXXX + ``` + + To configure npm, you can use the `npm login` command or use a configuration file with a TOKEN : + + ``` + echo "//registry.npmjs.org/:_authToken=YYYYYYYYYYYYYYYYYYYYYYYYYYYYYY" > .npmrc + ``` + +#### Using release-it tool + +There are 3 yarn scripts that can be run to do the release + +##### yarn release-beta + +Automatically calculates and presents 3 beta versions - patch, minor and major for you to choose ( or Other for manual input). + +``` +? Select increment (next version): +❯ prepatch (0.1.1-beta.0) + preminor (0.2.0-beta.0) + premajor (1.0.0-beta.0) + Other, please specify... +``` + +##### yarn release-major-beta + +Same as `yarn release-beta`, but with premajor version pre-selected. + +##### yarn release + +Generic command, does not automatically add the `beta` to version, but you can still manually write it if you choose Other. + +#### Important notes + +> Do not use release-it tool on master branch, the commit on CHANGELOG.md file and the version increase in the package.json file can't be done without a PULL REQUEST. + +> Do not keep Pull Requests from develop to master branches open when you are doing beta releases from the develop branch. As long as a PR to master is open, an automatic script will run on every commit and will update both the version and the changelog to a production-ready state - ( MAJOR.MINOR.PATCH mandatory format for version). + diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000..51bd52b --- /dev/null +++ b/babel.config.js @@ -0,0 +1,17 @@ +module.exports = function (api) { + api.cache(true); + const presets = ['razzle']; + const plugins = [ + [ + 'react-intl', // React Intl extractor, required for the whole i18n infrastructure to work + { + messagesDir: './build/messages/', + }, + ], + ]; + + return { + plugins, + presets, + }; +}; diff --git a/bootstrap b/bootstrap new file mode 100644 index 0000000..8613750 --- /dev/null +++ b/bootstrap @@ -0,0 +1,41 @@ +const path = require('path'); +const fs = require('fs'); +const ejs = require('ejs'); + +const currentDir = path.basename(process.cwd()); + +const bootstrap = function (ofile) { + fs.readFile(ofile, 'utf8', function (err, data) { + if (err) { + return console.log(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); + } + }); + } + }); +} + +fs.readdir(".", { withFileTypes: true }, (err, dirents) => { + const files = dirents + .filter(dirent => dirent.isFile()) + .map(dirent => dirent.name); + files.forEach(function (file) { + if (file != 'bootstrap') { + bootstrap(file); + } + }); +}); diff --git a/cypress.config.js b/cypress.config.js new file mode 100644 index 0000000..da8e7ba --- /dev/null +++ b/cypress.config.js @@ -0,0 +1,27 @@ +const { defineConfig } = require('cypress'); + +module.exports = defineConfig({ + viewportWidth: 1280, + defaultCommandTimeout: 8888, + chromeWebSecurity: false, + reporter: 'junit', + video: true, + retries: { + runMode: 8, + openMode: 0, + }, + reporterOptions: { + mochaFile: 'cypress/reports/cypress-[hash].xml', + jenkinsMode: true, + toConsole: true, + }, + e2e: { + setupNodeEvents(on, config) { + // e2e testing node events setup code + require('@cypress/code-coverage/task')(on, config); + require('cypress-fail-fast/plugin')(on, config); + return config; + }, + baseUrl: 'http://localhost:3000', + }, +}); diff --git a/cypress/e2e/01-block-basics.cy.js b/cypress/e2e/01-block-basics.cy.js new file mode 100644 index 0000000..089c7b3 --- /dev/null +++ b/cypress/e2e/01-block-basics.cy.js @@ -0,0 +1,29 @@ +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(); + + // 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/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/support/commands.js b/cypress/support/commands.js new file mode 100644 index 0000000..f269a7f --- /dev/null +++ b/cypress/support/commands.js @@ -0,0 +1,536 @@ +/* eslint no-console: ["error", { allow: ["log"] }] */ + +const SLATE_SELECTOR = '.content-area .slate-editor [contenteditable=true]'; +const SLATE_TITLE_SELECTOR = '.block.inner.title [contenteditable="true"]'; + +// --- 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').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().type(type); +}); + +Cypress.Commands.add('setSlateCursor', (subject, query, endQuery) => { + cy.get('.slate-editor.selected [contenteditable=true]') + .focus() + .click() + .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 new file mode 100644 index 0000000..b7c02f2 --- /dev/null +++ b/cypress/support/e2e.js @@ -0,0 +1,128 @@ +// *********************************************************** +// 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') + +//Generate code-coverage +import '@cypress/code-coverage/support'; + +// Fail Fast +import "cypress-fail-fast"; + +export const slateBeforeEach = (contentType = 'Document') => { + 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.waitForResourceToLoad('@navigation'); + cy.waitForResourceToLoad('@breadcrumbs'); + cy.waitForResourceToLoad('@actions'); + cy.waitForResourceToLoad('@types'); + cy.waitForResourceToLoad('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(); + + return s1; +}; + +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/jest-addon.config.js b/jest-addon.config.js new file mode 100644 index 0000000..da38318 --- /dev/null +++ b/jest-addon.config.js @@ -0,0 +1,36 @@ +module.exports = { + testMatch: ['**/src/addons/**/?(*.)+(spec|test).[jt]s?(x)'], + collectCoverageFrom: [ + 'src/addons/**/src/**/*.{js,jsx,ts,tsx}', + '!src/**/*.d.ts', + ], + moduleNameMapper: { + '@plone/volto/cypress': '