diff --git a/.github/workflows/e2e b/.github/workflows/e2e deleted file mode 100644 index 338869490..000000000 --- a/.github/workflows/e2e +++ /dev/null @@ -1,44 +0,0 @@ -name: E2E - -on: [push, pull_request] - -permissions: - contents: read - -jobs: - build: - runs-on: ubuntu-latest - strategy: - matrix: - node-version: [16.x] - steps: - - uses: actions/checkout@v3 - - - name: Setup Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 - with: - cache: yarn - node-version: ${{ matrix.node-version }} - - - name: Install Yarn - run: npm i yarn --global - - - name: Install Packages - run: | - yarn install --frozen-lockfile - env: - CYPRESS_CACHE_FOLDER: .cache/Cypress - - - name: Run Build - run: yarn build - - - name: Run E2E Tests - run: yarn e2e - env: - CYPRESS_CACHE_FOLDER: .cache/Cypress - - - name: Upload Coverage to Coveralls - uses: coverallsapp/github-action@master - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - flag-name: e2e diff --git a/.github/workflows/e2e-applitools.yml b/.github/workflows/e2e-applitools.yml new file mode 100644 index 000000000..b0319b072 --- /dev/null +++ b/.github/workflows/e2e-applitools.yml @@ -0,0 +1,73 @@ +name: E2E (Applitools) + +on: + workflow_dispatch: + # Because we want to limit Applitools usage, so we only want to start this + # workflow on rare occasions/manually. + inputs: + parent_branch: + required: true + type: string + default: master + description: 'Parent branch to use for PRs' + +permissions: + contents: read + +env: + # on PRs from forks, this secret will always be empty, for security reasons + USE_APPLI: ${{ secrets.APPLITOOLS_API_KEY && 'true' || '' }} + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [16.x] + steps: + - if: ${{ ! env.USE_APPLI }} + name: Warn if not using Applitools + run: | + echo "::error,title=Not using Applitols::APPLITOOLS_API_KEY is empty, disabling Applitools for this run." + - uses: actions/checkout@v3 + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + cache: yarn + node-version: ${{ matrix.node-version }} + + - name: Install Yarn + run: npm i yarn --global + + - name: Install Packages + run: | + yarn install --frozen-lockfile + env: + CYPRESS_CACHE_FOLDER: .cache/Cypress + + - name: Run Build + run: yarn build + + - if: ${{ env.USE_APPLI }} + name: Notify applitools of new batch + # Copied from docs https://applitools.com/docs/topics/integrations/github-integration-ci-setup.html + run: curl -L -d '' -X POST "$APPLITOOLS_SERVER_URL/api/externals/github/push?apiKey=$APPLITOOLS_API_KEY&CommitSha=$GITHUB_SHA&BranchName=${APPLITOOLS_BRANCH}$&ParentBranchName=$APPLITOOLS_PARENT_BRANCH" + env: + # e.g. mermaid-js/mermaid/my-branch + APPLITOOLS_BRANCH: ${{ github.repository }}/${{ github.ref_name }} + APPLITOOLS_PARENT_BRANCH: ${{ github.inputs.parent_branch }} + APPLITOOLS_API_KEY: ${{ secrets.APPLITOOLS_API_KEY }} + APPLITOOLS_SERVER_URL: 'https://eyesapi.applitools.com' + + - name: Run E2E Tests + run: yarn e2e + env: + CYPRESS_CACHE_FOLDER: .cache/Cypress + # Mermaid applitools.config.js uses this to pick batch name. + APPLI_BRANCH: ${{ github.ref_name }} + APPLITOOLS_BATCH_ID: ${{ github.sha }} + # e.g. mermaid-js/mermaid/my-branch + APPLITOOLS_BRANCH: ${{ github.repository }}/${{ github.ref_name }} + APPLITOOLS_PARENT_BRANCH: ${{ github.inputs.parent_branch }} + APPLITOOLS_API_KEY: ${{ secrets.APPLITOOLS_API_KEY }} + APPLITOOLS_SERVER_URL: 'https://eyesapi.applitools.com' diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 000000000..06a346aeb --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,38 @@ +name: E2E + +on: [push, pull_request] + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [16.x] + steps: + - uses: actions/checkout@v3 + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + cache: yarn + node-version: ${{ matrix.node-version }} + + - name: Install Yarn + run: npm i yarn --global + + - name: Install Packages + run: | + yarn install --frozen-lockfile + env: + CYPRESS_CACHE_FOLDER: .cache/Cypress + + - name: Run Build + run: yarn build + + - name: Run E2E Tests + run: yarn e2e + env: + CYPRESS_CACHE_FOLDER: .cache/Cypress diff --git a/applitools.cnfig.js b/applitools.cnfig.js deleted file mode 100644 index 900aabf2e..000000000 --- a/applitools.cnfig.js +++ /dev/null @@ -1,16 +0,0 @@ -module.exports = { - testConcurrency: 1, - // browser: [ - // // Add browsers with different viewports - // { width: 800, height: 600, name: 'chrome' }, - // { width: 700, height: 500, name: 'firefox' }, - // { width: 1600, height: 1200, name: 'ie11' }, - // { width: 1024, height: 768, name: 'edgechromium' }, - // { width: 800, height: 600, name: 'safari' }, - // // Add mobile emulation devices in Portrait mode - // { deviceName: 'iPhone X', screenOrientation: 'portrait' }, - // { deviceName: 'Pixel 2', screenOrientation: 'portrait' }, - // ], - // // set batch name to the configuration - // batchName: 'Ultrafast Batch', -}; diff --git a/applitools.config.js b/applitools.config.js new file mode 100644 index 000000000..1c0607868 --- /dev/null +++ b/applitools.config.js @@ -0,0 +1,19 @@ +// eslint-disable-next-line @typescript-eslint/no-var-requires +const { defineConfig } = require('cypress'); + +module.exports = defineConfig({ + testConcurrency: 1, + browser: [ + // Add browsers with different viewports + // { width: 800, height: 600, name: 'chrome' }, + // { width: 700, height: 500, name: 'firefox' }, + // { width: 1600, height: 1200, name: 'ie11' }, + // { width: 1024, height: 768, name: 'edgechromium' }, + // { width: 800, height: 600, name: 'safari' }, + // // Add mobile emulation devices in Portrait mode + // { deviceName: 'iPhone X', screenOrientation: 'portrait' }, + // { deviceName: 'Pixel 2', screenOrientation: 'portrait' }, + ], + // set batch name to the configuration + batchName: `Mermaid ${process.env.APPLI_BRANCH ?? "'no APPLI_BRANCH set'"}`, +}); diff --git a/cypress.config.js b/cypress.config.js index d7c9831d4..b434cec47 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -2,21 +2,20 @@ const { defineConfig } = require('cypress'); const { addMatchImageSnapshotPlugin } = require('cypress-image-snapshot/plugin'); -require('@applitools/eyes-cypress')(module); module.exports = defineConfig({ e2e: { - specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}', + specPattern: 'cypress/integration/**/*.{js,jsx,ts,tsx}', setupNodeEvents(on, config) { addMatchImageSnapshotPlugin(on, config); // copy any needed variables from process.env to config.env config.env.useAppli = process.env.USE_APPLI ? true : false; - config.env.codeBranch = process.env.APPLI_BRANCH; // do not forget to return the changed config object! return config; }, - supportFile: 'cypress/support/index.js', }, video: false, }); + +require('@applitools/eyes-cypress')(module); diff --git a/cypress/downloads/downloads.html b/cypress/downloads/downloads.html deleted file mode 100644 index b42fc821e..000000000 --- a/cypress/downloads/downloads.html +++ /dev/null @@ -1 +0,0 @@ -Cr24 \ No newline at end of file diff --git a/cypress/helpers/util.js b/cypress/helpers/util.js index b5e64a1e8..d138dfcfe 100644 --- a/cypress/helpers/util.js +++ b/cypress/helpers/util.js @@ -44,15 +44,13 @@ export const imgSnapshotTest = (graphStr, _options, api = false, validation) => } const useAppli = Cypress.env('useAppli'); //const useAppli = false; - const branch = Cypress.env('codeBranch'); cy.log('Hello ' + useAppli ? 'Appli' : 'image-snapshot'); const name = (options.name || cy.state('runnable').fullTitle()).replace(/\s+/g, '-'); if (useAppli) { cy.eyesOpen({ - appName: 'Mermaid-' + branch, + appName: 'Mermaid', testName: name, - batchName: branch, }); } @@ -96,15 +94,13 @@ export const urlSnapshotTest = (url, _options, api = false, validation) => { options.fontSize = '16px'; } const useAppli = Cypress.env('useAppli'); - const branch = Cypress.env('codeBranch'); cy.log('Hello ' + useAppli ? 'Appli' : 'image-snapshot'); const name = (options.name || cy.state('runnable').fullTitle()).replace(/\s+/g, '-'); if (useAppli) { cy.eyesOpen({ - appName: 'Mermaid-' + branch, + appName: 'Mermaid', testName: name, - batchName: branch, }); } diff --git a/cypress/integration/other/configuration.spec.js b/cypress/integration/other/configuration.spec.js index a67758d9c..513cf0714 100644 --- a/cypress/integration/other/configuration.spec.js +++ b/cypress/integration/other/configuration.spec.js @@ -15,11 +15,13 @@ describe('Configuration', () => { // Check the marker-end property to make sure it is properly set to // start with # - cy.get('.edgePath path') - .first() - .should('have.attr', 'marker-end') - .should('exist') - .and('include', 'url(#'); + cy.get('.edgePaths').within(() => { + cy.get('path') + .first() + .should('have.attr', 'marker-end') + .should('exist') + .and('include', 'url(#'); + }); }); it('should handle default value false of arrowMarkerAbsolute', () => { renderGraph( @@ -35,11 +37,13 @@ describe('Configuration', () => { // Check the marker-end property to make sure it is properly set to // start with # - cy.get('.edgePath path') - .first() - .should('have.attr', 'marker-end') - .should('exist') - .and('include', 'url(#'); + cy.get('.edgePaths').within(() => { + cy.get('path') + .first() + .should('have.attr', 'marker-end') + .should('exist') + .and('include', 'url(#'); + }); }); it('should handle arrowMarkerAbsolute explicitly set to false', () => { renderGraph( @@ -57,11 +61,13 @@ describe('Configuration', () => { // Check the marker-end property to make sure it is properly set to // start with # - cy.get('.edgePath path') - .first() - .should('have.attr', 'marker-end') - .should('exist') - .and('include', 'url(#'); + cy.get('.edgePaths').within(() => { + cy.get('path') + .first() + .should('have.attr', 'marker-end') + .should('exist') + .and('include', 'url(#'); + }); }); it('should handle arrowMarkerAbsolute explicitly set to "false" as false', () => { renderGraph( @@ -79,15 +85,17 @@ describe('Configuration', () => { // Check the marker-end property to make sure it is properly set to // start with # - cy.get('.edgePath path') - .first() - .should('have.attr', 'marker-end') - .should('exist') - .and('include', 'url(#'); + cy.get('.edgePaths').within(() => { + cy.get('path') + .first() + .should('have.attr', 'marker-end') + .should('exist') + .and('include', 'url(#'); + }); }); it('should handle arrowMarkerAbsolute set to true', () => { renderGraph( - `graph TD + `flowchart TD A[Christmas] -->|Get money| B(Go shopping) B --> C{Let me think} C -->|One| D[Laptop] @@ -99,11 +107,13 @@ describe('Configuration', () => { } ); - cy.get('.edgePath path') - .first() - .should('have.attr', 'marker-end') - .should('exist') - .and('include', 'url(http://localhost'); + cy.get('.edgePaths').within(() => { + cy.get('path') + .first() + .should('have.attr', 'marker-end') + .should('exist') + .and('include', 'url(http://localhost'); + }); }); it('should not taint the initial configuration when using multiple directives', () => { const url = 'http://localhost:9000/regression/issue-1874.html'; diff --git a/cypress/integration/other/xss.spec.js b/cypress/integration/other/xss.spec.js index 912354f7d..4911dd8df 100644 --- a/cypress/integration/other/xss.spec.js +++ b/cypress/integration/other/xss.spec.js @@ -81,6 +81,9 @@ describe('XSS', () => { cy.get('#the-malware').should('not.exist'); }); it('should not allow manipulating antiscript to run javascript using onerror in state diagrams with dagre d3', () => { + cy.on('uncaught:exception', (_err, _runnable) => { + return false; // continue rendering even if there if mermaid throws an error + }); cy.visit('http://localhost:9000/xss9.html'); cy.wait(1000); cy.get('#the-malware').should('not.exist'); diff --git a/cypress/integration/rendering/flowchart.spec.js b/cypress/integration/rendering/flowchart.spec.js index b4e94d1ab..c4ef54fcf 100644 --- a/cypress/integration/rendering/flowchart.spec.js +++ b/cypress/integration/rendering/flowchart.spec.js @@ -745,13 +745,13 @@ describe('Graph', () => { cy.get('svg').should((svg) => { expect(svg).to.have.attr('width', '100%'); // expect(svg).to.have.attr('height'); - // use within because the absolute value can be slightly different depending on the environment ±5% + // use within because the absolute value can be slightly different depending on the environment ±10% // const height = parseFloat(svg.attr('height')); // expect(height).to.be.within(446 * 0.95, 446 * 1.05); const style = svg.attr('style'); expect(style).to.match(/^max-width: [\d.]+px;$/); const maxWidthValue = parseFloat(style.match(/[\d.]+/g).join('')); - expect(maxWidthValue).to.be.within(300 * 0.95, 300 * 1.05); + expect(maxWidthValue).to.be.within(300 * 0.9, 300 * 1.1); }); }); it('39: should render a flowchart when useMaxWidth is false', () => { @@ -768,9 +768,9 @@ describe('Graph', () => { cy.get('svg').should((svg) => { // const height = parseFloat(svg.attr('height')); const width = parseFloat(svg.attr('width')); - // use within because the absolute value can be slightly different depending on the environment ±5% + // use within because the absolute value can be slightly different depending on the environment ±10% // expect(height).to.be.within(446 * 0.95, 446 * 1.05); - expect(width).to.be.within(300 * 0.95, 300 * 1.05); + expect(width).to.be.within(300 * 0.9, 300 * 1.1); expect(svg).to.not.have.attr('style'); }); }); diff --git a/cypress/integration/rendering/gitGraph.spec.js b/cypress/integration/rendering/gitGraph.spec.js index 80981c31c..afb39b62e 100644 --- a/cypress/integration/rendering/gitGraph.spec.js +++ b/cypress/integration/rendering/gitGraph.spec.js @@ -180,7 +180,48 @@ describe('Git Graph diagram', () => { {} ); }); - + it('11: should render a gitgraph with cherry pick commit with custom tag', () => { + imgSnapshotTest( + ` + gitGraph + commit id: "ZERO" + branch develop + commit id:"A" + checkout main + commit id:"ONE" + checkout develop + commit id:"B" + checkout main + commit id:"TWO" + cherry-pick id:"A" tag: "snapshot" + commit id:"THREE" + checkout develop + commit id:"C" + `, + {} + ); + }); + it('11: should render a gitgraph with cherry pick commit with no tag', () => { + imgSnapshotTest( + ` + gitGraph + commit id: "ZERO" + branch develop + commit id:"A" + checkout main + commit id:"ONE" + checkout develop + commit id:"B" + checkout main + commit id:"TWO" + cherry-pick id:"A" tag: "" + commit id:"THREE" + checkout develop + commit id:"C" + `, + {} + ); + }); it('11: should render a simple gitgraph with two cherry pick commit', () => { imgSnapshotTest( ` @@ -207,48 +248,48 @@ describe('Git Graph diagram', () => { {} ); }); - it('12: should render commits for more than 8 branches', () => { imgSnapshotTest( ` gitGraph checkout main - commit + %% Make sure to manually set the ID of all commits, for consistent visual tests + commit id: "1-abcdefg" checkout main branch branch1 - commit + commit id: "2-abcdefg" checkout main merge branch1 branch branch2 - commit + commit id: "3-abcdefg" checkout main merge branch2 branch branch3 - commit + commit id: "4-abcdefg" checkout main merge branch3 branch branch4 - commit + commit id: "5-abcdefg" checkout main merge branch4 branch branch5 - commit + commit id: "6-abcdefg" checkout main merge branch5 branch branch6 - commit + commit id: "7-abcdefg" checkout main merge branch6 branch branch7 - commit + commit id: "8-abcdefg" checkout main merge branch7 branch branch8 - commit + commit id: "9-abcdefg" checkout main merge branch8 branch branch9 - commit + commit id: "10-abcdefg" `, {} ); diff --git a/cypress/platform/gitgraph.html b/cypress/platform/gitgraph.html index da81427f8..0186d6209 100644 --- a/cypress/platform/gitgraph.html +++ b/cypress/platform/gitgraph.html @@ -30,7 +30,31 @@

info below

+
+  %%{init: { "logLevel": "debug", "theme": "default" , "gitGraph" : {"showBranches":"false","rotateCommitLabel":"true"},"themeVariables": {
+              "gitBranchLabel0": "#ff0000",
+              "gitBranchLabel1": "#00ff00",
+              "gitBranchLabel2": "#0000ff",
+              "git0": "#550055"
+       } } }%%
+    gitGraph
+      commit
+       branch develop
+       commit
+       commit
+       branch release/1.0.0
+       checkout release/1.0.0
+       commit tag:"1.0.0-beta1"
+       checkout develop
+       commit
+       commit
+       commit
+       commit
+       checkout release/1.0.0
+       merge develop tag: "1.0.0-beta2"
+    
+
     %%{init: { "logLevel": "debug", "theme": "default" , "gitGraph" : {"showBranches":"false"},"themeVariables": {
               "gitBranchLabel0": "#ff0000",
               "gitBranchLabel1": "#00ff00",
@@ -131,6 +155,7 @@
         // arrowMarkerAbsolute: true,
         // themeCSS: '.edgePath .path {stroke: red;} .arrowheadPath {fill: red;}',
         logLevel: 1,
+        gitGraph: { rotateCommitLabel: false },
         flowchart: { curve: 'linear', htmlLabels: true },
         // gantt: { axisFormat: '%m/%d/%Y' },
         sequence: { actorMargin: 50, showSequenceNumbers: true },
diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js
deleted file mode 100644
index ca5a37d5a..000000000
--- a/cypress/plugins/index.js
+++ /dev/null
@@ -1,33 +0,0 @@
-/* eslint-disable @typescript-eslint/no-var-requires */
-// ***********************************************************
-// 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)
-
-// module.exports = (on, config) => {
-//   // `on` is used to hook into various events Cypress emits
-//   // `config` is the resolved Cypress config
-// }
-
-const { addMatchImageSnapshotPlugin } = require('cypress-image-snapshot/plugin');
-require('@applitools/eyes-cypress')(module);
-
-module.exports = (on, config) => {
-  addMatchImageSnapshotPlugin(on, config);
-  // copy any needed variables from process.env to config.env
-  config.env.useAppli = process.env.USE_APPLI ? true : false;
-  config.env.codeBranch = process.env.APPLI_BRANCH;
-
-  // do not forget to return the changed config object!
-  return config;
-};
-
-require('@applitools/eyes-cypress')(module);
diff --git a/cypress/support/index.js b/cypress/support/e2e.js
similarity index 90%
rename from cypress/support/index.js
rename to cypress/support/e2e.js
index 1d23c59bf..69d93b4a4 100644
--- a/cypress/support/index.js
+++ b/cypress/support/e2e.js
@@ -17,8 +17,6 @@ import '@applitools/eyes-cypress/commands';
 
 // Import commands.js using ES2015 syntax:
 import './commands';
-// import '@percy/cypress';
-import '@applitools/eyes-cypress/commands';
 
 // Alternatively you can use CommonJS syntax:
 // require('./commands')
diff --git a/docs/classDiagram.md b/docs/classDiagram.md
index 6c9ae96fe..3f12dddf8 100644
--- a/docs/classDiagram.md
+++ b/docs/classDiagram.md
@@ -660,14 +660,27 @@ It is also possible to attach a class to a list of nodes in one statement:
 
 A shorter form of adding a class is to attach the classname to the node using the `:::` operator:
 
-```mmd
+```mermaid-example
+classDiagram
+    class Animal:::cssClass
+```
+
+```mermaid
 classDiagram
     class Animal:::cssClass
 ```
 
 Or:
 
-```mmd
+```mermaid-example
+classDiagram
+    class Animal:::cssClass {
+        -int sizeInFeet
+        -canEat()
+    }
+```
+
+```mermaid
 classDiagram
     class Animal:::cssClass {
         -int sizeInFeet
diff --git a/package.json b/package.json
index e447426ae..2cf463b9d 100644
--- a/package.json
+++ b/package.json
@@ -101,7 +101,7 @@
     "concurrently": "^7.4.0",
     "coveralls": "^3.1.1",
     "css-to-string-loader": "^0.1.3",
-    "cypress": "9.7.0",
+    "cypress": "^10.0.0",
     "cypress-image-snapshot": "^4.0.1",
     "documentation": "13.2.0",
     "esbuild": "^0.15.6",
diff --git a/src/dagre-wrapper/edges.js b/src/dagre-wrapper/edges.js
index 86e41de1d..6ed08e924 100644
--- a/src/dagre-wrapper/edges.js
+++ b/src/dagre-wrapper/edges.js
@@ -472,7 +472,8 @@ export const insertEdge = function (elem, e, edge, clusterDb, diagramType, graph
   // });
 
   let url = '';
-  if (getConfig().state.arrowMarkerAbsolute) {
+  // // TODO: Can we load this config only from the rendered graph type?
+  if (getConfig().flowchart.arrowMarkerAbsolute || getConfig().state.arrowMarkerAbsolute) {
     url =
       window.location.protocol +
       '//' +
diff --git a/src/diagram-api/diagram-orchestration.ts b/src/diagram-api/diagram-orchestration.ts
index e5b5f3cc0..d68d4da79 100644
--- a/src/diagram-api/diagram-orchestration.ts
+++ b/src/diagram-api/diagram-orchestration.ts
@@ -96,6 +96,7 @@ import { journeyDetector } from '../diagrams/user-journey/journeyDetector';
 import journeyDb from '../diagrams/user-journey/journeyDb';
 import journeyRenderer from '../diagrams/user-journey/journeyRenderer';
 import journeyStyles from '../diagrams/user-journey/styles';
+import { getConfig, setConfig } from '../config';
 
 import errorRenderer from '../diagrams/error/errorRenderer';
 import errorStyles from '../diagrams/error/styles';
@@ -301,11 +302,12 @@ export const addDiagrams = () => {
       renderer: flowRendererV2,
       styles: flowStyles,
       init: (cnf) => {
-        flowRenderer.setConf(cnf.flowchart);
         if (!cnf.flowchart) {
           cnf.flowchart = {};
         }
+        // TODO, broken as of 2022-09-14 (13809b50251845475e6dca65cc395761be38fbd2)
         cnf.flowchart.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute;
+        flowRenderer.setConf(cnf.flowchart);
         flowDb.clear();
         flowDb.setGen('gen-1');
       },
@@ -320,11 +322,13 @@ export const addDiagrams = () => {
       renderer: flowRendererV2,
       styles: flowStyles,
       init: (cnf) => {
-        flowRendererV2.setConf(cnf.flowchart);
         if (!cnf.flowchart) {
           cnf.flowchart = {};
         }
         cnf.flowchart.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute;
+        // flowchart-v2 uses dagre-wrapper, which doesn't have access to flowchart cnf
+        setConfig({ flowchart: { arrowMarkerAbsolute: cnf.arrowMarkerAbsolute } });
+        flowRendererV2.setConf(cnf.flowchart);
         flowDb.clear();
         flowDb.setGen('gen-2');
       },
diff --git a/src/diagrams/git/gitGraphAst.js b/src/diagrams/git/gitGraphAst.js
index fb9bb100d..41130c780 100644
--- a/src/diagrams/git/gitGraphAst.js
+++ b/src/diagrams/git/gitGraphAst.js
@@ -258,9 +258,11 @@ export const merge = function (otherBranch, custom_id, override_type, custom_tag
   log.debug('in mergeBranch');
 };
 
-export const cherryPick = function (sourceId, targetId) {
+export const cherryPick = function (sourceId, targetId, tag) {
+  log.debug('Entering cherryPick:', sourceId, targetId, tag);
   sourceId = common.sanitizeText(sourceId, configApi.getConfig());
   targetId = common.sanitizeText(targetId, configApi.getConfig());
+  tag = common.sanitizeText(tag, configApi.getConfig());
 
   if (!sourceId || typeof commits[sourceId] === 'undefined') {
     let error = new Error(
@@ -328,13 +330,13 @@ export const cherryPick = function (sourceId, targetId) {
       parents: [head == null ? null : head.id, sourceCommit.id],
       branch: curBranch,
       type: commitType.CHERRY_PICK,
-      tag: 'cherry-pick:' + sourceCommit.id,
+      tag: tag ?? 'cherry-pick:' + sourceCommit.id,
     };
     head = commit;
     commits[commit.id] = commit;
     branches[curBranch] = commit.id;
     log.debug(branches);
-    log.debug('in cheeryPick');
+    log.debug('in cherryPick');
   }
 };
 export const checkout = function (branch) {
diff --git a/src/diagrams/git/gitGraphParserV2.spec.js b/src/diagrams/git/gitGraphParserV2.spec.js
index b6c9c2459..7aab8fc7c 100644
--- a/src/diagrams/git/gitGraphParserV2.spec.js
+++ b/src/diagrams/git/gitGraphParserV2.spec.js
@@ -611,6 +611,54 @@ describe('when parsing a gitGraph', function () {
     ]);
   });
 
+  it('should support cherry-picking commits', function () {
+    const str = `gitGraph
+    commit id: "ZERO"
+    branch develop
+    commit id:"A"
+    checkout main
+    cherry-pick id:"A"
+    `;
+
+    parser.parse(str);
+    const commits = parser.yy.getCommits();
+    const cherryPickCommitID = Object.keys(commits)[2];
+    expect(commits[cherryPickCommitID].tag).toBe('cherry-pick:A');
+    expect(commits[cherryPickCommitID].branch).toBe('main');
+  });
+
+  it('should support cherry-picking commits with custom tag', function () {
+    const str = `gitGraph
+    commit id: "ZERO"
+    branch develop
+    commit id:"A"
+    checkout main
+    cherry-pick id:"A" tag:"MyTag"
+    `;
+
+    parser.parse(str);
+    const commits = parser.yy.getCommits();
+    const cherryPickCommitID = Object.keys(commits)[2];
+    expect(commits[cherryPickCommitID].tag).toBe('MyTag');
+    expect(commits[cherryPickCommitID].branch).toBe('main');
+  });
+
+  it('should support cherry-picking commits with no tag', function () {
+    const str = `gitGraph
+    commit id: "ZERO"
+    branch develop
+    commit id:"A"
+    checkout main
+    cherry-pick id:"A" tag:""
+    `;
+
+    parser.parse(str);
+    const commits = parser.yy.getCommits();
+    const cherryPickCommitID = Object.keys(commits)[2];
+    expect(commits[cherryPickCommitID].tag).toBe('');
+    expect(commits[cherryPickCommitID].branch).toBe('main');
+  });
+
   it('should throw error when try to branch existing branch: main', function () {
     const str = `gitGraph
     commit
diff --git a/src/diagrams/git/gitGraphRenderer.js b/src/diagrams/git/gitGraphRenderer.js
index 68905c0d2..e15e43ac3 100644
--- a/src/diagrams/git/gitGraphRenderer.js
+++ b/src/diagrams/git/gitGraphRenderer.js
@@ -1,7 +1,6 @@
 import { select } from 'd3';
-import { configureSvgSize } from '../../setupGraphViewbox';
+import { getConfig, setupGraphViewbox } from '../../diagram-api/diagramAPI';
 import { log } from '../../logger';
-import { getConfig } from '../../config';
 import addSVGAccessibilityFields from '../../accessibility';
 
 let allCommitsDict = {};
@@ -523,18 +522,8 @@ export const draw = function (txt, id, ver, diagObj) {
   drawArrows(diagram, allCommitsDict);
   drawCommits(diagram, allCommitsDict, true);
 
-  const padding = gitGraphConfig.diagramPadding;
-  const svgBounds = diagram.node().getBBox();
-  const width = svgBounds.width + padding * 2;
-  const height = svgBounds.height + padding * 2;
-
-  configureSvgSize(diagram, height, width, conf.useMaxWidth);
-  const vBox = `${
-    svgBounds.x -
-    padding -
-    (gitGraphConfig.showBranches && gitGraphConfig.rotateCommitLabel === true ? 30 : 0)
-  } ${svgBounds.y - padding} ${width} ${height}`;
-  diagram.attr('viewBox', vBox);
+  // Setup the view box and size of the svg element
+  setupGraphViewbox(undefined, diagram, gitGraphConfig.diagramPadding, conf.useMaxWidth);
 };
 
 export default {
diff --git a/src/diagrams/git/parser/gitGraph.jison b/src/diagrams/git/parser/gitGraph.jison
index f35dbcde3..dbe220e15 100644
--- a/src/diagrams/git/parser/gitGraph.jison
+++ b/src/diagrams/git/parser/gitGraph.jison
@@ -47,7 +47,7 @@ commit(?=\s|$)                          return 'COMMIT';
 branch(?=\s|$)                          return 'BRANCH';
 "order:"                                return 'ORDER';
 merge(?=\s|$)                           return 'MERGE';
-cherry-pick(?=\s|$)                     return 'CHERRY_PICK';
+cherry\-pick(?=\s|$)                    return 'CHERRY_PICK';
 // "reset"                                 return 'RESET';
 checkout(?=\s|$)                        return 'CHECKOUT';
 "LR"                                    return 'DIR';
@@ -57,6 +57,7 @@ checkout(?=\s|$)                        return 'CHECKOUT';
 "options"\r?\n                          this.begin("options"); //
 [ \r\n\t]+"end"                this.popState();       // not used anymore in the renderer, fixed for backward compatibility
 [\s\S]+(?=[ \r\n\t]+"end")     return 'OPT';          //
+["]["]                                  return 'EMPTYSTR';
 ["]                                     this.begin("string");
 ["]                             this.popState();
 [^"]*                           return 'STR';
@@ -117,7 +118,11 @@ branchStatement
     ;
 
 cherryPickStatement
-    : CHERRY_PICK COMMIT_ID STR {yy.cherryPick($3)}
+    : CHERRY_PICK COMMIT_ID STR {yy.cherryPick($3, '', undefined)}
+    | CHERRY_PICK COMMIT_ID STR COMMIT_TAG STR {yy.cherryPick($3, '', $5)}
+    | CHERRY_PICK COMMIT_ID STR COMMIT_TAG EMPTYSTR {yy.cherryPick($3, '', '')}
+    | CHERRY_PICK COMMIT_TAG STR COMMIT_ID STR {yy.cherryPick($5, '', $3)}
+    | CHERRY_PICK COMMIT_TAG EMPTYSTR COMMIT_ID STR {yy.cherryPick($3, '', '')}
     ;
 
 mergeStatement
diff --git a/src/docs/classDiagram.md b/src/docs/classDiagram.md
index 87d76cd37..f46d3689c 100644
--- a/src/docs/classDiagram.md
+++ b/src/docs/classDiagram.md
@@ -493,14 +493,14 @@ It is also possible to attach a class to a list of nodes in one statement:
 
 A shorter form of adding a class is to attach the classname to the node using the `:::` operator:
 
-```mmd
+```mermaid-example
 classDiagram
     class Animal:::cssClass
 ```
 
 Or:
 
-```mmd
+```mermaid-example
 classDiagram
     class Animal:::cssClass {
         -int sizeInFeet
diff --git a/yarn.lock b/yarn.lock
index c71948568..e19910efe 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3485,9 +3485,9 @@ camelcase@^5.0.0, camelcase@^5.3.1:
   integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
 
 caniuse-lite@^1.0.30001359:
-  version "1.0.30001397"
-  resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001397.tgz"
-  integrity sha512-SW9N2TbCdLf0eiNDRrrQXx2sOkaakNZbCjgNpPyMJJbiOrU5QzMIrXOVMRM1myBXTD5iTkdrtU/EguCrBocHlA==
+  version "1.0.30001406"
+  resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001406.tgz"
+  integrity sha512-bWTlaXUy/rq0BBtYShc/jArYfBPjEV95euvZ8JVtO43oQExEN/WquoqpufFjNu4kSpi5cy5kMbNvzztWDfv1Jg==
 
 caseless@~0.12.0:
   version "0.12.0"
@@ -4248,10 +4248,10 @@ cypress-image-snapshot@^4.0.1:
     pkg-dir "^3.0.0"
     term-img "^4.0.0"
 
-cypress@9.7.0:
-  version "9.7.0"
-  resolved "https://registry.yarnpkg.com/cypress/-/cypress-9.7.0.tgz#bf55b2afd481f7a113ef5604aa8b693564b5e744"
-  integrity sha512-+1EE1nuuuwIt/N1KXRR2iWHU+OiIt7H28jJDyyI4tiUftId/DrXYEwoDa5+kH2pki1zxnA0r6HrUGHV5eLbF5Q==
+cypress@^10.0.0:
+  version "10.8.0"
+  resolved "https://registry.yarnpkg.com/cypress/-/cypress-10.8.0.tgz#12a681f2642b6f13d636bab65d5b71abdb1497a5"
+  integrity sha512-QVse0dnLm018hgti2enKMVZR9qbIO488YGX06nH5j3Dg1isL38DwrBtyrax02CANU6y8F4EJUuyW6HJKw1jsFA==
   dependencies:
     "@cypress/request" "^2.88.10"
     "@cypress/xvfb" "^1.2.4"
@@ -4272,7 +4272,7 @@ cypress@9.7.0:
     dayjs "^1.10.4"
     debug "^4.3.2"
     enquirer "^2.3.6"
-    eventemitter2 "^6.4.3"
+    eventemitter2 "6.4.7"
     execa "4.1.0"
     executable "^4.1.1"
     extract-zip "2.0.1"
@@ -5647,10 +5647,10 @@ event-target-shim@^5.0.0:
   resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
   integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
 
-eventemitter2@^6.4.3:
-  version "6.4.5"
-  resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.5.tgz#97380f758ae24ac15df8353e0cc27f8b95644655"
-  integrity sha512-bXE7Dyc1i6oQElDG0jMRZJrRAn9QR2xyyFGmBdZleNmyQX0FqGYmhZIrIrpPfm/w//LTo4tVQGOGQcGCb5q9uw==
+eventemitter2@6.4.7:
+  version "6.4.7"
+  resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.7.tgz#a7f6c4d7abf28a14c1ef3442f21cb306a054271d"
+  integrity sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==
 
 eventemitter3@^4.0.0:
   version "4.0.7"