Compare commits

..

71 Commits
8.2.5 ... 8.3.0

Author SHA1 Message Date
knsv
0200fef389 Release 8.3.0 2019-09-18 22:19:48 -07:00
knsv
d8397f146b #937 Handling direction keywords in node ids 2019-09-18 12:56:24 -07:00
Knut Sveidqvist
244be86207 E2e test of interaction in graphs 2019-09-18 19:42:19 +02:00
Knut Sveidqvist
c26135780d Adding build to travis steps 2019-09-18 19:01:58 +02:00
Knut Sveidqvist
4411a26002 Cleanup, removed old e2e in favour of cypress 2019-09-18 18:37:53 +02:00
Knut Sveidqvist
21622f575b Updating cypress tests 2019-09-18 18:25:06 +02:00
Knut Sveidqvist
23e6df04d4 Merge pull request #929 from knsv/dependabot/npm_and_yarn/eslint-utils-1.4.2
chore(deps): bump eslint-utils from 1.3.1 to 1.4.2
2019-09-18 18:08:09 +02:00
Knut Sveidqvist
bf403dfc62 Merge pull request #933 from Dunning-Kruger/features/close-stale-issues
Close stale issues
2019-09-18 18:06:57 +02:00
Knut Sveidqvist
c3f48b5a13 Merge pull request #936 from janverb/fix-configuration-defaults-htmllabels-value
Correctly document htmlLabels as true by default
2019-09-18 13:51:16 +02:00
Jan Verbeek
e192454f54 Correctly document htmlLabels as true by default
The "mermaidAPI configuration defaults" section in the documentation
listed `htmlLabels` as `false`, but the real default is `true`, as
seen in the code and elsewhere in the documentation.
2019-09-18 13:05:08 +02:00
Ignacio Orlandoni
a55573be94 Update stale.yml
Stalebot now closes inactive stale issues.
2019-09-13 14:15:49 -04:00
dependabot[bot]
0e548f63f9 chore(deps): bump eslint-utils from 1.3.1 to 1.4.2
Bumps [eslint-utils](https://github.com/mysticatea/eslint-utils) from 1.3.1 to 1.4.2.
- [Release notes](https://github.com/mysticatea/eslint-utils/releases)
- [Commits](https://github.com/mysticatea/eslint-utils/compare/v1.3.1...v1.4.2)

Signed-off-by: dependabot[bot] <support@github.com>
2019-09-12 20:20:09 +00:00
Knut Sveidqvist
4d1a34661e Merge pull request #932 from knsv/i931_code_standard
I931 code standard
2019-09-12 22:18:46 +02:00
knsv
7e5802e799 #931 Last code standard fixes 2019-09-12 12:59:13 -07:00
knsv
0e8164d805 #931 Compliacne with code standard 2019-09-12 12:58:57 -07:00
knsv
f9b30bdb43 #931 Reformatting for compliacne with code standard 2019-09-12 12:58:32 -07:00
knsv
34de31195f #931 Aligning with code standard 2019-09-12 12:58:04 -07:00
knsv
cf05a8d8fa #931 Updating code to align witn new code standard 2019-09-12 12:57:36 -07:00
knsv
a2e3f3d900 #931 Reformatting code for gitGraph 2019-09-12 12:56:54 -07:00
knsv
0890ba0fdd #931 replacing linter 2019-09-12 12:56:20 -07:00
knsv
d2f082b2e2 #931 Replacing linter 2019-09-12 12:55:56 -07:00
knsv
e67b8c86d6 #931 Aligning code standard 2019-09-12 12:55:31 -07:00
knsv
e14922f15c #931 Aligning code standard 2019-09-12 12:55:20 -07:00
knsv
ad5669b523 #931 Aligning code standard 2019-09-12 12:55:10 -07:00
knsv
f2a6ba80b5 #931 Aligning code standard 2019-09-12 12:54:59 -07:00
knsv
b3dac15d57 Merge branch 'master' into i931_code_standard 2019-09-12 12:51:41 -07:00
Knut Sveidqvist
def4ca699a #931 added conf and libraries 2019-09-12 21:03:49 +02:00
knsv
f98fa82134 Adding percy badge 2019-09-11 13:57:53 -07:00
Knut Sveidqvist
ff44671ae5 Merge pull request #928 from knsv/feature/Issue-22_Pie-Chart-Feature
#22 Basic Pie Chart
2019-09-11 21:50:42 +02:00
Ashish Jain
398d66bda9 #22 Update SCSS for pie chart specific title class 2019-09-11 21:39:39 +02:00
Ashish Jain
eb9ac1bbe5 #22 Updated yarn.lock 2019-09-11 21:29:31 +02:00
Ashish Jain
42fc23cff2 #22 Basic Pie Chart 2019-09-11 21:20:28 +02:00
Knut Sveidqvist
78cae3dce7 Finding the missing cypress binary 2019-09-11 20:23:41 +02:00
Knut Sveidqvist
beed86ff37 Update for accessing missing binary 2019-09-11 20:20:15 +02:00
Knut Sveidqvist
ec7324e12e Restore documention written in autoigenerated file 2019-09-11 20:03:22 +02:00
Knut Sveidqvist
d097b673bb #927 enabling the e2e tests 2019-09-11 19:23:08 +02:00
Knut Sveidqvist
7eea957a3b #927 Upgrading node 2019-09-11 19:18:55 +02:00
Knut Sveidqvist
4dda6b8a81 #927 Updated yarn.lock 2019-09-11 19:16:15 +02:00
Knut Sveidqvist
5fd5a65283 #927 reverted changes 2019-09-11 19:04:30 +02:00
Knut Sveidqvist
ca0513396d Investigation of build issues 2019-09-11 18:59:44 +02:00
Knut Sveidqvist
9f87ab4941 #927 Adding support for cypress and Percy 2019-09-11 18:53:05 +02:00
knsv
e37f5a6eb2 #926 Applying the color styling on the label instead of the node 2019-09-08 02:56:06 -07:00
knsv
ece40cdc54 #926 E2e test for issue 2019-09-08 00:33:56 -07:00
knsv
65561b22c5 #926 Adding e2e tools for replicating issues 2019-09-08 00:33:38 -07:00
knsv
21aa8c5f15 #922 Fix for click binding on nodes with ids starting with a number 2019-09-03 11:31:47 -07:00
knsv
f4bafacc62 Release 8.2.6 2019-09-01 04:16:09 -07:00
knsv
2cb54293f8 Lint fixes 2019-09-01 02:18:00 -07:00
knsv
5610185050 #918 Removed som logging 2019-09-01 00:45:24 -07:00
knsv
699bd61045 #918 Fix for issue with nodes starting with a number in a subgraph 2019-09-01 00:44:48 -07:00
Knut Sveidqvist
27d0b934a1 Merge pull request #917 from knsv/dependabot/npm_and_yarn/mixin-deep-1.3.2
chore(deps): bump mixin-deep from 1.3.1 to 1.3.2
2019-08-29 17:16:43 +02:00
Knut Sveidqvist
0f1b704385 Merge pull request #912 from knsv/dependabot/npm_and_yarn/eslint-utils-1.4.2
chore(deps): bump eslint-utils from 1.3.1 to 1.4.2
2019-08-29 17:16:28 +02:00
Knut Sveidqvist
7d0c1d2594 Merge pull request #909 from davidpendraykalibrate/patch-1
Fix typos in README
2019-08-29 17:15:24 +02:00
dependabot[bot]
43cff9e1a3 chore(deps): bump mixin-deep from 1.3.1 to 1.3.2
Bumps [mixin-deep](https://github.com/jonschlinkert/mixin-deep) from 1.3.1 to 1.3.2.
- [Release notes](https://github.com/jonschlinkert/mixin-deep/releases)
- [Commits](https://github.com/jonschlinkert/mixin-deep/compare/1.3.1...1.3.2)

Signed-off-by: dependabot[bot] <support@github.com>
2019-08-29 02:30:07 +00:00
erelling
f5ddf869e4 Link and clarification 2019-08-28 22:01:19 +02:00
erelling
185db4c112 Update mermaidAPI.md 2019-08-28 21:57:10 +02:00
erelling
07e3815b74 Update mermaidAPI.md 2019-08-28 21:56:27 +02:00
erelling
187ddfd80c Clarification 2019-08-28 21:55:35 +02:00
erelling
d3f7923bc6 Formatting, defaults example 2019-08-28 21:50:44 +02:00
erelling
e52db94c1a Formatting 2019-08-28 21:26:38 +02:00
erelling
e50959c1fe Formatting 2019-08-28 21:14:07 +02:00
erelling
bab75625cb Configuration example 2 2019-08-28 21:13:38 +02:00
erelling
f0b039ad10 Security level, update for new syntax 2019-08-28 21:08:27 +02:00
erelling
5c8dc7ab07 Better formatting 2019-08-28 21:03:23 +02:00
erelling
de2b5390cb Syntax clarification and full example 2019-08-28 20:59:28 +02:00
Knut Sveidqvist
d89ceb2125 Merge branch 'master' of github.com:knsv/mermaid 2019-08-28 20:25:59 +02:00
Knut Sveidqvist
c43d58d3c9 #916 Allow chaining of vertice in flowcharts 2019-08-28 20:25:54 +02:00
Eduardas Michelsonas
357c47910a Test commit 2019-08-28 18:53:49 +02:00
Knut Sveidqvist
4ae48f4284 #915 Reviving the possibility to use underscore in text in vertices 2019-08-28 17:34:57 +02:00
knsv
a48a306fe8 #914 Fix for issue identifying node wehen the id of the node starts with a digit 2019-08-27 12:16:11 -07:00
dependabot[bot]
a3c1928fc0 chore(deps): bump eslint-utils from 1.3.1 to 1.4.2
Bumps [eslint-utils](https://github.com/mysticatea/eslint-utils) from 1.3.1 to 1.4.2.
- [Release notes](https://github.com/mysticatea/eslint-utils/releases)
- [Commits](https://github.com/mysticatea/eslint-utils/compare/v1.3.1...v1.4.2)

Signed-off-by: dependabot[bot] <support@github.com>
2019-08-26 18:25:33 +00:00
David Pendray
dbcd4f635e Fix typos in README 2019-08-22 11:53:26 +01:00
133 changed files with 10936 additions and 6373 deletions

18
.eslintrc.json Normal file
View File

@@ -0,0 +1,18 @@
{
"env": {
"browser": true,
"es6": true
},
"parserOptions": {
"ecmaFeatures": {
"experimentalObjectRestSpread": true,
"jsx": true
},
"sourceType": "module"
},
"extends": ["prettier"],
"plugins": ["prettier"],
"rules": {
"prettier/prettier": ["error"]
}
}

6
.github/stale.yml vendored
View File

@@ -1,7 +1,7 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 60
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
daysUntilClose: 14
# Issues with these labels will never be considered stale
exemptLabels:
- Status: Pinned
@@ -16,4 +16,6 @@ markComment: >
for your contributions.
If you are still interested in this issue and it is still relevant you can comment to revive it.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false
closeComment: >
This issue has been been automatically closed due to a lack of activity.
This is done to maintain a clean list of issues that the community is interested in developing.

3
.percy.yml Normal file
View File

@@ -0,0 +1,3 @@
version: 1
snapshot:
widths: [1280]

4
.prettierrc Normal file
View File

@@ -0,0 +1,4 @@
{
"printWidth": 100,
"singleQuote": true
}

22
.tern-project Normal file
View File

@@ -0,0 +1,22 @@
{
"ecmaVersion": 6,
"libs": [
"browser"
],
"loadEagerly": [
"path/to/your/js/**/*.js"
],
"dontLoad": [
"node_modules/**",
"path/to/your/js/**/*.js"
],
"plugins": {
"modules": {},
"es_modules": {},
"node": {},
"doc_comment": {
"fullDocs": true,
"strong": true
}
}
}

View File

@@ -1,8 +1,12 @@
dist: trusty
language: node_js
node_js:
- "8"
- "10"
cache:
npm: false
script:
- yarn build
- yarn test --coverage
- yarn e2e
after_success:
- cat ./coverage/lcov.info | ./node_modules/.bin/coveralls

View File

@@ -1,6 +1,7 @@
[![Build Status](https://travis-ci.org/knsv/mermaid.svg?branch=master)](https://travis-ci.org/knsv/mermaid)
[![Coverage Status](https://coveralls.io/repos/github/knsv/mermaid/badge.svg?branch=master)](https://coveralls.io/github/knsv/mermaid?branch=master)
[![Join the chat at https://gitter.im/knsv/mermaid](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/knsv/mermaid?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![This project is using Percy.io for visual regression testing.](https://percy.io/static/images/percy-badge.svg)](https://percy.io/Mermaid/mermaid)
# mermaid
@@ -13,7 +14,7 @@ In version 8.2 a security improvement was introduced. A securityLevel configurat
⚠️ **Note** : This changes the default behaviour of mermaid so that after upgrade to 8.2, if the securityLevel is not configured, tags in flowcharts are encoded as tags and clicking is prohibited.
If your application is taking resposibility for the diagram source security you can set the securityLevel accordingly. By doing this clicks and tags are again allowed.
If your application is taking responsibility for the diagram source security you can set the securityLevel accordingly. By doing this clicks and tags are again allowed.
```javascript
mermaidAPI.initialize({
@@ -31,7 +32,7 @@ Ever wanted to simplify documentation and avoid heavy tools like Visio when expl
This is why mermaid was born, a simple markdown-like script language for generating charts from text via javascript.
**Mermaid was nomiated and won the JS Open Source Awards (2019) in the category _The most exciting use of technology_!!! Thanks to all involved, people committing pull requests, people answering questions and special thanks to Tyler Long who is helping me maintain the project.**
**Mermaid was nominated and won the JS Open Source Awards (2019) in the category _The most exciting use of technology_!!! Thanks to all involved, people committing pull requests, people answering questions and special thanks to Tyler Long who is helping me maintain the project.**
### Flowchart

1
cypress.json Normal file
View File

@@ -0,0 +1 @@
{ "video": false }

View File

@@ -0,0 +1,272 @@
/// <reference types="Cypress" />
context('Actions', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/actions')
})
// https://on.cypress.io/interacting-with-elements
it('.type() - type into a DOM element', () => {
// https://on.cypress.io/type
cy.get('.action-email')
.type('fake@email.com').should('have.value', 'fake@email.com')
// .type() with special character sequences
.type('{leftarrow}{rightarrow}{uparrow}{downarrow}')
.type('{del}{selectall}{backspace}')
// .type() with key modifiers
.type('{alt}{option}') //these are equivalent
.type('{ctrl}{control}') //these are equivalent
.type('{meta}{command}{cmd}') //these are equivalent
.type('{shift}')
// Delay each keypress by 0.1 sec
.type('slow.typing@email.com', { delay: 100 })
.should('have.value', 'slow.typing@email.com')
cy.get('.action-disabled')
// Ignore error checking prior to type
// like whether the input is visible or disabled
.type('disabled error checking', { force: true })
.should('have.value', 'disabled error checking')
})
it('.focus() - focus on a DOM element', () => {
// https://on.cypress.io/focus
cy.get('.action-focus').focus()
.should('have.class', 'focus')
.prev().should('have.attr', 'style', 'color: orange;')
})
it('.blur() - blur off a DOM element', () => {
// https://on.cypress.io/blur
cy.get('.action-blur').type('About to blur').blur()
.should('have.class', 'error')
.prev().should('have.attr', 'style', 'color: red;')
})
it('.clear() - clears an input or textarea element', () => {
// https://on.cypress.io/clear
cy.get('.action-clear').type('Clear this text')
.should('have.value', 'Clear this text')
.clear()
.should('have.value', '')
})
it('.submit() - submit a form', () => {
// https://on.cypress.io/submit
cy.get('.action-form')
.find('[type="text"]').type('HALFOFF')
cy.get('.action-form').submit()
.next().should('contain', 'Your form has been submitted!')
})
it('.click() - click on a DOM element', () => {
// https://on.cypress.io/click
cy.get('.action-btn').click()
// You can click on 9 specific positions of an element:
// -----------------------------------
// | topLeft top topRight |
// | |
// | |
// | |
// | left center right |
// | |
// | |
// | |
// | bottomLeft bottom bottomRight |
// -----------------------------------
// clicking in the center of the element is the default
cy.get('#action-canvas').click()
cy.get('#action-canvas').click('topLeft')
cy.get('#action-canvas').click('top')
cy.get('#action-canvas').click('topRight')
cy.get('#action-canvas').click('left')
cy.get('#action-canvas').click('right')
cy.get('#action-canvas').click('bottomLeft')
cy.get('#action-canvas').click('bottom')
cy.get('#action-canvas').click('bottomRight')
// .click() accepts an x and y coordinate
// that controls where the click occurs :)
cy.get('#action-canvas')
.click(80, 75) // click 80px on x coord and 75px on y coord
.click(170, 75)
.click(80, 165)
.click(100, 185)
.click(125, 190)
.click(150, 185)
.click(170, 165)
// click multiple elements by passing multiple: true
cy.get('.action-labels>.label').click({ multiple: true })
// Ignore error checking prior to clicking
cy.get('.action-opacity>.btn').click({ force: true })
})
it('.dblclick() - double click on a DOM element', () => {
// https://on.cypress.io/dblclick
// Our app has a listener on 'dblclick' event in our 'scripts.js'
// that hides the div and shows an input on double click
cy.get('.action-div').dblclick().should('not.be.visible')
cy.get('.action-input-hidden').should('be.visible')
})
it('.check() - check a checkbox or radio element', () => {
// https://on.cypress.io/check
// By default, .check() will check all
// matching checkbox or radio elements in succession, one after another
cy.get('.action-checkboxes [type="checkbox"]').not('[disabled]')
.check().should('be.checked')
cy.get('.action-radios [type="radio"]').not('[disabled]')
.check().should('be.checked')
// .check() accepts a value argument
cy.get('.action-radios [type="radio"]')
.check('radio1').should('be.checked')
// .check() accepts an array of values
cy.get('.action-multiple-checkboxes [type="checkbox"]')
.check(['checkbox1', 'checkbox2']).should('be.checked')
// Ignore error checking prior to checking
cy.get('.action-checkboxes [disabled]')
.check({ force: true }).should('be.checked')
cy.get('.action-radios [type="radio"]')
.check('radio3', { force: true }).should('be.checked')
})
it('.uncheck() - uncheck a checkbox element', () => {
// https://on.cypress.io/uncheck
// By default, .uncheck() will uncheck all matching
// checkbox elements in succession, one after another
cy.get('.action-check [type="checkbox"]')
.not('[disabled]')
.uncheck().should('not.be.checked')
// .uncheck() accepts a value argument
cy.get('.action-check [type="checkbox"]')
.check('checkbox1')
.uncheck('checkbox1').should('not.be.checked')
// .uncheck() accepts an array of values
cy.get('.action-check [type="checkbox"]')
.check(['checkbox1', 'checkbox3'])
.uncheck(['checkbox1', 'checkbox3']).should('not.be.checked')
// Ignore error checking prior to unchecking
cy.get('.action-check [disabled]')
.uncheck({ force: true }).should('not.be.checked')
})
it('.select() - select an option in a <select> element', () => {
// https://on.cypress.io/select
// Select option(s) with matching text content
cy.get('.action-select').select('apples')
cy.get('.action-select-multiple')
.select(['apples', 'oranges', 'bananas'])
// Select option(s) with matching value
cy.get('.action-select').select('fr-bananas')
cy.get('.action-select-multiple')
.select(['fr-apples', 'fr-oranges', 'fr-bananas'])
})
it('.scrollIntoView() - scroll an element into view', () => {
// https://on.cypress.io/scrollintoview
// normally all of these buttons are hidden,
// because they're not within
// the viewable area of their parent
// (we need to scroll to see them)
cy.get('#scroll-horizontal button')
.should('not.be.visible')
// scroll the button into view, as if the user had scrolled
cy.get('#scroll-horizontal button').scrollIntoView()
.should('be.visible')
cy.get('#scroll-vertical button')
.should('not.be.visible')
// Cypress handles the scroll direction needed
cy.get('#scroll-vertical button').scrollIntoView()
.should('be.visible')
cy.get('#scroll-both button')
.should('not.be.visible')
// Cypress knows to scroll to the right and down
cy.get('#scroll-both button').scrollIntoView()
.should('be.visible')
})
it('.trigger() - trigger an event on a DOM element', () => {
// https://on.cypress.io/trigger
// To interact with a range input (slider)
// we need to set its value & trigger the
// event to signal it changed
// Here, we invoke jQuery's val() method to set
// the value and trigger the 'change' event
cy.get('.trigger-input-range')
.invoke('val', 25)
.trigger('change')
.get('input[type=range]').siblings('p')
.should('have.text', '25')
})
it('cy.scrollTo() - scroll the window or element to a position', () => {
// https://on.cypress.io/scrollTo
// You can scroll to 9 specific positions of an element:
// -----------------------------------
// | topLeft top topRight |
// | |
// | |
// | |
// | left center right |
// | |
// | |
// | |
// | bottomLeft bottom bottomRight |
// -----------------------------------
// if you chain .scrollTo() off of cy, we will
// scroll the entire window
cy.scrollTo('bottom')
cy.get('#scrollable-horizontal').scrollTo('right')
// or you can scroll to a specific coordinate:
// (x axis, y axis) in pixels
cy.get('#scrollable-vertical').scrollTo(250, 250)
// or you can scroll to a specific percentage
// of the (width, height) of the element
cy.get('#scrollable-both').scrollTo('75%', '25%')
// control the easing of the scroll (default is 'swing')
cy.get('#scrollable-vertical').scrollTo('center', { easing: 'linear' })
// control the duration of the scroll (in ms)
cy.get('#scrollable-both').scrollTo('center', { duration: 2000 })
})
})

View File

@@ -0,0 +1,42 @@
/// <reference types="Cypress" />
context('Aliasing', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/aliasing')
})
it('.as() - alias a DOM element for later use', () => {
// https://on.cypress.io/as
// Alias a DOM element for use later
// We don't have to traverse to the element
// later in our code, we reference it with @
cy.get('.as-table').find('tbody>tr')
.first().find('td').first()
.find('button').as('firstBtn')
// when we reference the alias, we place an
// @ in front of its name
cy.get('@firstBtn').click()
cy.get('@firstBtn')
.should('have.class', 'btn-success')
.and('contain', 'Changed')
})
it('.as() - alias a route for later use', () => {
// Alias the route to wait for its response
cy.server()
cy.route('GET', 'comments/*').as('getComment')
// we have code that gets a comment when
// the button is clicked in scripts.js
cy.get('.network-btn').click()
// https://on.cypress.io/wait
cy.wait('@getComment').its('status').should('eq', 200)
})
})

View File

@@ -0,0 +1,168 @@
/// <reference types="Cypress" />
context('Assertions', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/assertions')
})
describe('Implicit Assertions', () => {
it('.should() - make an assertion about the current subject', () => {
// https://on.cypress.io/should
cy.get('.assertion-table')
.find('tbody tr:last')
.should('have.class', 'success')
.find('td')
.first()
// checking the text of the <td> element in various ways
.should('have.text', 'Column content')
.should('contain', 'Column content')
.should('have.html', 'Column content')
// chai-jquery uses "is()" to check if element matches selector
.should('match', 'td')
// to match text content against a regular expression
// first need to invoke jQuery method text()
// and then match using regular expression
.invoke('text')
.should('match', /column content/i)
// a better way to check element's text content against a regular expression
// is to use "cy.contains"
// https://on.cypress.io/contains
cy.get('.assertion-table')
.find('tbody tr:last')
// finds first <td> element with text content matching regular expression
.contains('td', /column content/i)
.should('be.visible')
// for more information about asserting element's text
// see https://on.cypress.io/using-cypress-faq#How-do-I-get-an-elements-text-contents
})
it('.and() - chain multiple assertions together', () => {
// https://on.cypress.io/and
cy.get('.assertions-link')
.should('have.class', 'active')
.and('have.attr', 'href')
.and('include', 'cypress.io')
})
})
describe('Explicit Assertions', () => {
// https://on.cypress.io/assertions
it('expect - make an assertion about a specified subject', () => {
// We can use Chai's BDD style assertions
expect(true).to.be.true
const o = { foo: 'bar' }
expect(o).to.equal(o)
expect(o).to.deep.equal({ foo: 'bar' })
// matching text using regular expression
expect('FooBar').to.match(/bar$/i)
})
it('pass your own callback function to should()', () => {
// Pass a function to should that can have any number
// of explicit assertions within it.
// The ".should(cb)" function will be retried
// automatically until it passes all your explicit assertions or times out.
cy.get('.assertions-p')
.find('p')
.should(($p) => {
// https://on.cypress.io/$
// return an array of texts from all of the p's
// @ts-ignore TS6133 unused variable
const texts = $p.map((i, el) => Cypress.$(el).text())
// jquery map returns jquery object
// and .get() convert this to simple array
const paragraphs = texts.get()
// array should have length of 3
expect(paragraphs, 'has 3 paragraphs').to.have.length(3)
// use second argument to expect(...) to provide clear
// message with each assertion
expect(paragraphs, 'has expected text in each paragraph').to.deep.eq([
'Some text from first p',
'More text from second p',
'And even more text from third p',
])
})
})
it('finds element by class name regex', () => {
cy.get('.docs-header')
.find('div')
// .should(cb) callback function will be retried
.should(($div) => {
expect($div).to.have.length(1)
const className = $div[0].className
expect(className).to.match(/heading-/)
})
// .then(cb) callback is not retried,
// it either passes or fails
.then(($div) => {
expect($div, 'text content').to.have.text('Introduction')
})
})
it('can throw any error', () => {
cy.get('.docs-header')
.find('div')
.should(($div) => {
if ($div.length !== 1) {
// you can throw your own errors
throw new Error('Did not find 1 element')
}
const className = $div[0].className
if (!className.match(/heading-/)) {
throw new Error(`Could not find class "heading-" in ${className}`)
}
})
})
it('matches unknown text between two elements', () => {
/**
* Text from the first element.
* @type {string}
*/
let text
/**
* Normalizes passed text,
* useful before comparing text with spaces and different capitalization.
* @param {string} s Text to normalize
*/
const normalizeText = (s) => s.replace(/\s/g, '').toLowerCase()
cy.get('.two-elements')
.find('.first')
.then(($first) => {
// save text from the first element
text = normalizeText($first.text())
})
cy.get('.two-elements')
.find('.second')
.should(($div) => {
// we can massage text before comparing
const secondText = normalizeText($div.text())
expect(secondText, 'second text').to.equal(text)
})
})
it('assert - assert shape of an object', () => {
const person = {
name: 'Joe',
age: 20,
}
assert.isObject(person, 'value is object')
})
})
})

View File

@@ -0,0 +1,56 @@
/// <reference types="Cypress" />
context('Connectors', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/connectors')
})
it('.each() - iterate over an array of elements', () => {
// https://on.cypress.io/each
cy.get('.connectors-each-ul>li')
.each(($el, index, $list) => {
console.log($el, index, $list)
})
})
it('.its() - get properties on the current subject', () => {
// https://on.cypress.io/its
cy.get('.connectors-its-ul>li')
// calls the 'length' property yielding that value
.its('length')
.should('be.gt', 2)
})
it('.invoke() - invoke a function on the current subject', () => {
// our div is hidden in our script.js
// $('.connectors-div').hide()
// https://on.cypress.io/invoke
cy.get('.connectors-div').should('be.hidden')
// call the jquery method 'show' on the 'div.container'
.invoke('show')
.should('be.visible')
})
it('.spread() - spread an array as individual args to callback function', () => {
// https://on.cypress.io/spread
const arr = ['foo', 'bar', 'baz']
cy.wrap(arr).spread((foo, bar, baz) => {
expect(foo).to.eq('foo')
expect(bar).to.eq('bar')
expect(baz).to.eq('baz')
})
})
it('.then() - invoke a callback function with the current subject', () => {
// https://on.cypress.io/then
cy.get('.connectors-list > li')
.then(($lis) => {
expect($lis, '3 items').to.have.length(3)
expect($lis.eq(0), 'first item').to.contain('Walk the dog')
expect($lis.eq(1), 'second item').to.contain('Feed the cat')
expect($lis.eq(2), 'third item').to.contain('Write JavaScript')
})
})
})

View File

@@ -0,0 +1,78 @@
/// <reference types="Cypress" />
context('Cookies', () => {
beforeEach(() => {
Cypress.Cookies.debug(true)
cy.visit('https://example.cypress.io/commands/cookies')
// clear cookies again after visiting to remove
// any 3rd party cookies picked up such as cloudflare
cy.clearCookies()
})
it('cy.getCookie() - get a browser cookie', () => {
// https://on.cypress.io/getcookie
cy.get('#getCookie .set-a-cookie').click()
// cy.getCookie() yields a cookie object
cy.getCookie('token').should('have.property', 'value', '123ABC')
})
it('cy.getCookies() - get browser cookies', () => {
// https://on.cypress.io/getcookies
cy.getCookies().should('be.empty')
cy.get('#getCookies .set-a-cookie').click()
// cy.getCookies() yields an array of cookies
cy.getCookies().should('have.length', 1).should((cookies) => {
// each cookie has these properties
expect(cookies[0]).to.have.property('name', 'token')
expect(cookies[0]).to.have.property('value', '123ABC')
expect(cookies[0]).to.have.property('httpOnly', false)
expect(cookies[0]).to.have.property('secure', false)
expect(cookies[0]).to.have.property('domain')
expect(cookies[0]).to.have.property('path')
})
})
it('cy.setCookie() - set a browser cookie', () => {
// https://on.cypress.io/setcookie
cy.getCookies().should('be.empty')
cy.setCookie('foo', 'bar')
// cy.getCookie() yields a cookie object
cy.getCookie('foo').should('have.property', 'value', 'bar')
})
it('cy.clearCookie() - clear a browser cookie', () => {
// https://on.cypress.io/clearcookie
cy.getCookie('token').should('be.null')
cy.get('#clearCookie .set-a-cookie').click()
cy.getCookie('token').should('have.property', 'value', '123ABC')
// cy.clearCookies() yields null
cy.clearCookie('token').should('be.null')
cy.getCookie('token').should('be.null')
})
it('cy.clearCookies() - clear browser cookies', () => {
// https://on.cypress.io/clearcookies
cy.getCookies().should('be.empty')
cy.get('#clearCookies .set-a-cookie').click()
cy.getCookies().should('have.length', 1)
// cy.clearCookies() yields null
cy.clearCookies()
cy.getCookies().should('be.empty')
})
})

View File

@@ -0,0 +1,222 @@
/// <reference types="Cypress" />
context('Cypress.Commands', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api')
})
// https://on.cypress.io/custom-commands
it('.add() - create a custom command', () => {
Cypress.Commands.add('console', {
prevSubject: true,
}, (subject, method) => {
// the previous subject is automatically received
// and the commands arguments are shifted
// allow us to change the console method used
method = method || 'log'
// log the subject to the console
// @ts-ignore TS7017
console[method]('The subject is', subject)
// whatever we return becomes the new subject
// we don't want to change the subject so
// we return whatever was passed in
return subject
})
// @ts-ignore TS2339
cy.get('button').console('info').then(($button) => {
// subject is still $button
})
})
})
context('Cypress.Cookies', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api')
})
// https://on.cypress.io/cookies
it('.debug() - enable or disable debugging', () => {
Cypress.Cookies.debug(true)
// Cypress will now log in the console when
// cookies are set or cleared
cy.setCookie('fakeCookie', '123ABC')
cy.clearCookie('fakeCookie')
cy.setCookie('fakeCookie', '123ABC')
cy.clearCookie('fakeCookie')
cy.setCookie('fakeCookie', '123ABC')
})
it('.preserveOnce() - preserve cookies by key', () => {
// normally cookies are reset after each test
cy.getCookie('fakeCookie').should('not.be.ok')
// preserving a cookie will not clear it when
// the next test starts
cy.setCookie('lastCookie', '789XYZ')
Cypress.Cookies.preserveOnce('lastCookie')
})
it('.defaults() - set defaults for all cookies', () => {
// now any cookie with the name 'session_id' will
// not be cleared before each new test runs
Cypress.Cookies.defaults({
whitelist: 'session_id',
})
})
})
context('Cypress.Server', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api')
})
// Permanently override server options for
// all instances of cy.server()
// https://on.cypress.io/cypress-server
it('.defaults() - change default config of server', () => {
Cypress.Server.defaults({
delay: 0,
force404: false,
})
})
})
context('Cypress.arch', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api')
})
it('Get CPU architecture name of underlying OS', () => {
// https://on.cypress.io/arch
expect(Cypress.arch).to.exist
})
})
context('Cypress.config()', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api')
})
it('Get and set configuration options', () => {
// https://on.cypress.io/config
let myConfig = Cypress.config()
expect(myConfig).to.have.property('animationDistanceThreshold', 5)
expect(myConfig).to.have.property('baseUrl', null)
expect(myConfig).to.have.property('defaultCommandTimeout', 4000)
expect(myConfig).to.have.property('requestTimeout', 5000)
expect(myConfig).to.have.property('responseTimeout', 30000)
expect(myConfig).to.have.property('viewportHeight', 660)
expect(myConfig).to.have.property('viewportWidth', 1000)
expect(myConfig).to.have.property('pageLoadTimeout', 60000)
expect(myConfig).to.have.property('waitForAnimations', true)
expect(Cypress.config('pageLoadTimeout')).to.eq(60000)
// this will change the config for the rest of your tests!
Cypress.config('pageLoadTimeout', 20000)
expect(Cypress.config('pageLoadTimeout')).to.eq(20000)
Cypress.config('pageLoadTimeout', 60000)
})
})
context('Cypress.dom', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api')
})
// https://on.cypress.io/dom
it('.isHidden() - determine if a DOM element is hidden', () => {
let hiddenP = Cypress.$('.dom-p p.hidden').get(0)
let visibleP = Cypress.$('.dom-p p.visible').get(0)
// our first paragraph has css class 'hidden'
expect(Cypress.dom.isHidden(hiddenP)).to.be.true
expect(Cypress.dom.isHidden(visibleP)).to.be.false
})
})
context('Cypress.env()', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api')
})
// We can set environment variables for highly dynamic values
// https://on.cypress.io/environment-variables
it('Get environment variables', () => {
// https://on.cypress.io/env
// set multiple environment variables
Cypress.env({
host: 'veronica.dev.local',
api_server: 'http://localhost:8888/v1/',
})
// get environment variable
expect(Cypress.env('host')).to.eq('veronica.dev.local')
// set environment variable
Cypress.env('api_server', 'http://localhost:8888/v2/')
expect(Cypress.env('api_server')).to.eq('http://localhost:8888/v2/')
// get all environment variable
expect(Cypress.env()).to.have.property('host', 'veronica.dev.local')
expect(Cypress.env()).to.have.property('api_server', 'http://localhost:8888/v2/')
})
})
context('Cypress.log', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api')
})
it('Control what is printed to the Command Log', () => {
// https://on.cypress.io/cypress-log
})
})
context('Cypress.platform', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api')
})
it('Get underlying OS name', () => {
// https://on.cypress.io/platform
expect(Cypress.platform).to.be.exist
})
})
context('Cypress.version', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api')
})
it('Get current version of Cypress being run', () => {
// https://on.cypress.io/version
expect(Cypress.version).to.be.exist
})
})
context('Cypress.spec', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api')
})
it('Get current spec information', () => {
// https://on.cypress.io/spec
// wrap the object so we can inspect it easily by clicking in the command log
cy.wrap(Cypress.spec).should('have.keys', ['name', 'relative', 'absolute'])
})
})

View File

@@ -0,0 +1,114 @@
/// <reference types="Cypress" />
/// JSON fixture file can be loaded directly using
// the built-in JavaScript bundler
// @ts-ignore
const requiredExample = require('../../fixtures/example')
context('Files', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/files')
})
beforeEach(() => {
// load example.json fixture file and store
// in the test context object
cy.fixture('example.json').as('example')
})
it('cy.fixture() - load a fixture', () => {
// https://on.cypress.io/fixture
// Instead of writing a response inline you can
// use a fixture file's content.
cy.server()
cy.fixture('example.json').as('comment')
// when application makes an Ajax request matching "GET comments/*"
// Cypress will intercept it and reply with object
// from the "comment" alias
cy.route('GET', 'comments/*', '@comment').as('getComment')
// we have code that gets a comment when
// the button is clicked in scripts.js
cy.get('.fixture-btn').click()
cy.wait('@getComment').its('responseBody')
.should('have.property', 'name')
.and('include', 'Using fixtures to represent data')
// you can also just write the fixture in the route
cy.route('GET', 'comments/*', 'fixture:example.json').as('getComment')
// we have code that gets a comment when
// the button is clicked in scripts.js
cy.get('.fixture-btn').click()
cy.wait('@getComment').its('responseBody')
.should('have.property', 'name')
.and('include', 'Using fixtures to represent data')
// or write fx to represent fixture
// by default it assumes it's .json
cy.route('GET', 'comments/*', 'fx:example').as('getComment')
// we have code that gets a comment when
// the button is clicked in scripts.js
cy.get('.fixture-btn').click()
cy.wait('@getComment').its('responseBody')
.should('have.property', 'name')
.and('include', 'Using fixtures to represent data')
})
it('cy.fixture() or require - load a fixture', function () {
// we are inside the "function () { ... }"
// callback and can use test context object "this"
// "this.example" was loaded in "beforeEach" function callback
expect(this.example, 'fixture in the test context')
.to.deep.equal(requiredExample)
// or use "cy.wrap" and "should('deep.equal', ...)" assertion
// @ts-ignore
cy.wrap(this.example, 'fixture vs require')
.should('deep.equal', requiredExample)
})
it('cy.readFile() - read a files contents', () => {
// https://on.cypress.io/readfile
// You can read a file and yield its contents
// The filePath is relative to your project's root.
cy.readFile('cypress.json').then((json) => {
expect(json).to.be.an('object')
})
})
it('cy.writeFile() - write to a file', () => {
// https://on.cypress.io/writefile
// You can write to a file
// Use a response from a request to automatically
// generate a fixture file for use later
cy.request('https://jsonplaceholder.cypress.io/users')
.then((response) => {
cy.writeFile('cypress/fixtures/users.json', response.body)
})
cy.fixture('users').should((users) => {
expect(users[0].name).to.exist
})
// JavaScript arrays and objects are stringified
// and formatted into text.
cy.writeFile('cypress/fixtures/profile.json', {
id: 8739,
name: 'Jane',
email: 'jane@example.com',
})
cy.fixture('profile').should((profile) => {
expect(profile.name).to.eq('Jane')
})
})
})

View File

@@ -0,0 +1,52 @@
/// <reference types="Cypress" />
context('Local Storage', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/local-storage')
})
// Although local storage is automatically cleared
// in between tests to maintain a clean state
// sometimes we need to clear the local storage manually
it('cy.clearLocalStorage() - clear all data in local storage', () => {
// https://on.cypress.io/clearlocalstorage
cy.get('.ls-btn').click().should(() => {
expect(localStorage.getItem('prop1')).to.eq('red')
expect(localStorage.getItem('prop2')).to.eq('blue')
expect(localStorage.getItem('prop3')).to.eq('magenta')
})
// clearLocalStorage() yields the localStorage object
cy.clearLocalStorage().should((ls) => {
expect(ls.getItem('prop1')).to.be.null
expect(ls.getItem('prop2')).to.be.null
expect(ls.getItem('prop3')).to.be.null
})
// Clear key matching string in Local Storage
cy.get('.ls-btn').click().should(() => {
expect(localStorage.getItem('prop1')).to.eq('red')
expect(localStorage.getItem('prop2')).to.eq('blue')
expect(localStorage.getItem('prop3')).to.eq('magenta')
})
cy.clearLocalStorage('prop1').should((ls) => {
expect(ls.getItem('prop1')).to.be.null
expect(ls.getItem('prop2')).to.eq('blue')
expect(ls.getItem('prop3')).to.eq('magenta')
})
// Clear keys matching regex in Local Storage
cy.get('.ls-btn').click().should(() => {
expect(localStorage.getItem('prop1')).to.eq('red')
expect(localStorage.getItem('prop2')).to.eq('blue')
expect(localStorage.getItem('prop3')).to.eq('magenta')
})
cy.clearLocalStorage(/prop1|2/).should((ls) => {
expect(ls.getItem('prop1')).to.be.null
expect(ls.getItem('prop2')).to.be.null
expect(ls.getItem('prop3')).to.eq('magenta')
})
})
})

View File

@@ -0,0 +1,32 @@
/// <reference types="Cypress" />
context('Location', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/location')
})
it('cy.hash() - get the current URL hash', () => {
// https://on.cypress.io/hash
cy.hash().should('be.empty')
})
it('cy.location() - get window.location', () => {
// https://on.cypress.io/location
cy.location().should((location) => {
expect(location.hash).to.be.empty
expect(location.href).to.eq('https://example.cypress.io/commands/location')
expect(location.host).to.eq('example.cypress.io')
expect(location.hostname).to.eq('example.cypress.io')
expect(location.origin).to.eq('https://example.cypress.io')
expect(location.pathname).to.eq('/commands/location')
expect(location.port).to.eq('')
expect(location.protocol).to.eq('https:')
expect(location.search).to.be.empty
})
})
it('cy.url() - get the current URL', () => {
// https://on.cypress.io/url
cy.url().should('eq', 'https://example.cypress.io/commands/location')
})
})

View File

@@ -0,0 +1,83 @@
/// <reference types="Cypress" />
context('Misc', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/misc')
})
it('.end() - end the command chain', () => {
// https://on.cypress.io/end
// cy.end is useful when you want to end a chain of commands
// and force Cypress to re-query from the root element
cy.get('.misc-table').within(() => {
// ends the current chain and yields null
cy.contains('Cheryl').click().end()
// queries the entire table again
cy.contains('Charles').click()
})
})
it('cy.exec() - execute a system command', () => {
// https://on.cypress.io/exec
// execute a system command.
// so you can take actions necessary for
// your test outside the scope of Cypress.
cy.exec('echo Jane Lane')
.its('stdout').should('contain', 'Jane Lane')
// we can use Cypress.platform string to
// select appropriate command
// https://on.cypress/io/platform
cy.log(`Platform ${Cypress.platform} architecture ${Cypress.arch}`)
if (Cypress.platform === 'win32') {
cy.exec('print cypress.json')
.its('stderr').should('be.empty')
} else {
cy.exec('cat cypress.json')
.its('stderr').should('be.empty')
cy.exec('pwd')
.its('code').should('eq', 0)
}
})
it('cy.focused() - get the DOM element that has focus', () => {
// https://on.cypress.io/focused
cy.get('.misc-form').find('#name').click()
cy.focused().should('have.id', 'name')
cy.get('.misc-form').find('#description').click()
cy.focused().should('have.id', 'description')
})
context('Cypress.Screenshot', function () {
it('cy.screenshot() - take a screenshot', () => {
// https://on.cypress.io/screenshot
cy.screenshot('my-image')
})
it('Cypress.Screenshot.defaults() - change default config of screenshots', function () {
Cypress.Screenshot.defaults({
blackout: ['.foo'],
capture: 'viewport',
clip: { x: 0, y: 0, width: 200, height: 200 },
scale: false,
disableTimersAndAnimations: true,
screenshotOnRunFailure: true,
beforeScreenshot () { },
afterScreenshot () { },
})
})
})
it('cy.wrap() - wrap an object', () => {
// https://on.cypress.io/wrap
cy.wrap({ foo: 'bar' })
.should('have.property', 'foo')
.and('include', 'bar')
})
})

View File

@@ -0,0 +1,56 @@
/// <reference types="Cypress" />
context('Navigation', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io')
cy.get('.navbar-nav').contains('Commands').click()
cy.get('.dropdown-menu').contains('Navigation').click()
})
it('cy.go() - go back or forward in the browser\'s history', () => {
// https://on.cypress.io/go
cy.location('pathname').should('include', 'navigation')
cy.go('back')
cy.location('pathname').should('not.include', 'navigation')
cy.go('forward')
cy.location('pathname').should('include', 'navigation')
// clicking back
cy.go(-1)
cy.location('pathname').should('not.include', 'navigation')
// clicking forward
cy.go(1)
cy.location('pathname').should('include', 'navigation')
})
it('cy.reload() - reload the page', () => {
// https://on.cypress.io/reload
cy.reload()
// reload the page without using the cache
cy.reload(true)
})
it('cy.visit() - visit a remote url', () => {
// https://on.cypress.io/visit
// Visit any sub-domain of your current domain
// Pass options to the visit
cy.visit('https://example.cypress.io/commands/navigation', {
timeout: 50000, // increase total time for the visit to resolve
onBeforeLoad (contentWindow) {
// contentWindow is the remote page's window object
expect(typeof contentWindow === 'object').to.be.true
},
onLoad (contentWindow) {
// contentWindow is the remote page's window object
expect(typeof contentWindow === 'object').to.be.true
},
})
})
})

View File

@@ -0,0 +1,194 @@
/// <reference types="Cypress" />
context('Network Requests', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/network-requests')
})
// Manage AJAX / XHR requests in your app
it('cy.server() - control behavior of network requests and responses', () => {
// https://on.cypress.io/server
cy.server().should((server) => {
// the default options on server
// you can override any of these options
expect(server.delay).to.eq(0)
expect(server.method).to.eq('GET')
expect(server.status).to.eq(200)
expect(server.headers).to.be.null
expect(server.response).to.be.null
expect(server.onRequest).to.be.undefined
expect(server.onResponse).to.be.undefined
expect(server.onAbort).to.be.undefined
// These options control the server behavior
// affecting all requests
// pass false to disable existing route stubs
expect(server.enable).to.be.true
// forces requests that don't match your routes to 404
expect(server.force404).to.be.false
// whitelists requests from ever being logged or stubbed
expect(server.whitelist).to.be.a('function')
})
cy.server({
method: 'POST',
delay: 1000,
status: 422,
response: {},
})
// any route commands will now inherit the above options
// from the server. anything we pass specifically
// to route will override the defaults though.
})
it('cy.request() - make an XHR request', () => {
// https://on.cypress.io/request
cy.request('https://jsonplaceholder.cypress.io/comments')
.should((response) => {
expect(response.status).to.eq(200)
expect(response.body).to.have.length(500)
expect(response).to.have.property('headers')
expect(response).to.have.property('duration')
})
})
it('cy.request() - verify response using BDD syntax', () => {
cy.request('https://jsonplaceholder.cypress.io/comments')
.then((response) => {
// https://on.cypress.io/assertions
expect(response).property('status').to.equal(200)
expect(response).property('body').to.have.length(500)
expect(response).to.include.keys('headers', 'duration')
})
})
it('cy.request() with query parameters', () => {
// will execute request
// https://jsonplaceholder.cypress.io/comments?postId=1&id=3
cy.request({
url: 'https://jsonplaceholder.cypress.io/comments',
qs: {
postId: 1,
id: 3,
},
})
.its('body')
.should('be.an', 'array')
.and('have.length', 1)
.its('0') // yields first element of the array
.should('contain', {
postId: 1,
id: 3,
})
})
it('cy.request() - pass result to the second request', () => {
// first, let's find out the userId of the first user we have
cy.request('https://jsonplaceholder.cypress.io/users?_limit=1')
.its('body.0') // yields the first element of the returned list
.then((user) => {
expect(user).property('id').to.be.a('number')
// make a new post on behalf of the user
cy.request('POST', 'https://jsonplaceholder.cypress.io/posts', {
userId: user.id,
title: 'Cypress Test Runner',
body: 'Fast, easy and reliable testing for anything that runs in a browser.',
})
})
// note that the value here is the returned value of the 2nd request
// which is the new post object
.then((response) => {
expect(response).property('status').to.equal(201) // new entity created
expect(response).property('body').to.contain({
id: 101, // there are already 100 posts, so new entity gets id 101
title: 'Cypress Test Runner',
})
// we don't know the user id here - since it was in above closure
// so in this test just confirm that the property is there
expect(response.body).property('userId').to.be.a('number')
})
})
it('cy.request() - save response in the shared test context', () => {
// https://on.cypress.io/variables-and-aliases
cy.request('https://jsonplaceholder.cypress.io/users?_limit=1')
.its('body.0') // yields the first element of the returned list
.as('user') // saves the object in the test context
.then(function () {
// NOTE 👀
// By the time this callback runs the "as('user')" command
// has saved the user object in the test context.
// To access the test context we need to use
// the "function () { ... }" callback form,
// otherwise "this" points at a wrong or undefined object!
cy.request('POST', 'https://jsonplaceholder.cypress.io/posts', {
userId: this.user.id,
title: 'Cypress Test Runner',
body: 'Fast, easy and reliable testing for anything that runs in a browser.',
})
.its('body').as('post') // save the new post from the response
})
.then(function () {
// When this callback runs, both "cy.request" API commands have finished
// and the test context has "user" and "post" objects set.
// Let's verify them.
expect(this.post, 'post has the right user id').property('userId').to.equal(this.user.id)
})
})
it('cy.route() - route responses to matching requests', () => {
// https://on.cypress.io/route
let message = 'whoa, this comment does not exist'
cy.server()
// Listen to GET to comments/1
cy.route('GET', 'comments/*').as('getComment')
// we have code that gets a comment when
// the button is clicked in scripts.js
cy.get('.network-btn').click()
// https://on.cypress.io/wait
cy.wait('@getComment').its('status').should('eq', 200)
// Listen to POST to comments
cy.route('POST', '/comments').as('postComment')
// we have code that posts a comment when
// the button is clicked in scripts.js
cy.get('.network-post').click()
cy.wait('@postComment')
// get the route
cy.get('@postComment').should((xhr) => {
expect(xhr.requestBody).to.include('email')
expect(xhr.requestHeaders).to.have.property('Content-Type')
expect(xhr.responseBody).to.have.property('name', 'Using POST in cy.route()')
})
// Stub a response to PUT comments/ ****
cy.route({
method: 'PUT',
url: 'comments/*',
status: 404,
response: { error: message },
delay: 500,
}).as('putComment')
// we have code that puts a comment when
// the button is clicked in scripts.js
cy.get('.network-put').click()
cy.wait('@putComment')
// our 404 statusCode logic in scripts.js executed
cy.get('.network-put-comment').should('contain', message)
})
})

View File

@@ -0,0 +1,87 @@
/// <reference types="Cypress" />
context('Querying', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/querying')
})
// The most commonly used query is 'cy.get()', you can
// think of this like the '$' in jQuery
it('cy.get() - query DOM elements', () => {
// https://on.cypress.io/get
cy.get('#query-btn').should('contain', 'Button')
cy.get('.query-btn').should('contain', 'Button')
cy.get('#querying .well>button:first').should('contain', 'Button')
// ↲
// Use CSS selectors just like jQuery
cy.get('[data-test-id="test-example"]').should('have.class', 'example')
// 'cy.get()' yields jQuery object, you can get its attribute
// by invoking `.attr()` method
cy.get('[data-test-id="test-example"]')
.invoke('attr', 'data-test-id')
.should('equal', 'test-example')
// or you can get element's CSS property
cy.get('[data-test-id="test-example"]')
.invoke('css', 'position')
.should('equal', 'static')
// or use assertions directly during 'cy.get()'
// https://on.cypress.io/assertions
cy.get('[data-test-id="test-example"]')
.should('have.attr', 'data-test-id', 'test-example')
.and('have.css', 'position', 'static')
})
it('cy.contains() - query DOM elements with matching content', () => {
// https://on.cypress.io/contains
cy.get('.query-list')
.contains('bananas')
.should('have.class', 'third')
// we can pass a regexp to `.contains()`
cy.get('.query-list')
.contains(/^b\w+/)
.should('have.class', 'third')
cy.get('.query-list')
.contains('apples')
.should('have.class', 'first')
// passing a selector to contains will
// yield the selector containing the text
cy.get('#querying')
.contains('ul', 'oranges')
.should('have.class', 'query-list')
cy.get('.query-button')
.contains('Save Form')
.should('have.class', 'btn')
})
it('.within() - query DOM elements within a specific element', () => {
// https://on.cypress.io/within
cy.get('.query-form').within(() => {
cy.get('input:first').should('have.attr', 'placeholder', 'Email')
cy.get('input:last').should('have.attr', 'placeholder', 'Password')
})
})
it('cy.root() - query the root DOM element', () => {
// https://on.cypress.io/root
// By default, root is the document
cy.root().should('match', 'html')
cy.get('.query-ul').within(() => {
// In this within, the root is now the ul DOM element
cy.root().should('have.class', 'query-ul')
})
})
})

View File

@@ -0,0 +1,95 @@
/// <reference types="Cypress" />
context('Spies, Stubs, and Clock', () => {
it('cy.spy() - wrap a method in a spy', () => {
// https://on.cypress.io/spy
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
const obj = {
foo () {},
}
const spy = cy.spy(obj, 'foo').as('anyArgs')
obj.foo()
expect(spy).to.be.called
})
it('cy.spy() retries until assertions pass', () => {
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
const obj = {
/**
* Prints the argument passed
* @param x {any}
*/
foo (x) {
console.log('obj.foo called with', x)
},
}
cy.spy(obj, 'foo').as('foo')
setTimeout(() => {
obj.foo('first')
}, 500)
setTimeout(() => {
obj.foo('second')
}, 2500)
cy.get('@foo').should('have.been.calledTwice')
})
it('cy.stub() - create a stub and/or replace a function with stub', () => {
// https://on.cypress.io/stub
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
const obj = {
/**
* prints both arguments to the console
* @param a {string}
* @param b {string}
*/
foo (a, b) {
console.log('a', a, 'b', b)
},
}
const stub = cy.stub(obj, 'foo').as('foo')
obj.foo('foo', 'bar')
expect(stub).to.be.called
})
it('cy.clock() - control time in the browser', () => {
// https://on.cypress.io/clock
// create the date in UTC so its always the same
// no matter what local timezone the browser is running in
const now = new Date(Date.UTC(2017, 2, 14)).getTime()
cy.clock(now)
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
cy.get('#clock-div').click()
.should('have.text', '1489449600')
})
it('cy.tick() - move time in the browser', () => {
// https://on.cypress.io/tick
// create the date in UTC so its always the same
// no matter what local timezone the browser is running in
const now = new Date(Date.UTC(2017, 2, 14)).getTime()
cy.clock(now)
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
cy.get('#tick-div').click()
.should('have.text', '1489449600')
cy.tick(10000) // 10 seconds passed
cy.get('#tick-div').click()
.should('have.text', '1489449610')
})
})

View File

@@ -0,0 +1,121 @@
/// <reference types="Cypress" />
context('Traversal', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/traversal')
})
it('.children() - get child DOM elements', () => {
// https://on.cypress.io/children
cy.get('.traversal-breadcrumb')
.children('.active')
.should('contain', 'Data')
})
it('.closest() - get closest ancestor DOM element', () => {
// https://on.cypress.io/closest
cy.get('.traversal-badge')
.closest('ul')
.should('have.class', 'list-group')
})
it('.eq() - get a DOM element at a specific index', () => {
// https://on.cypress.io/eq
cy.get('.traversal-list>li')
.eq(1).should('contain', 'siamese')
})
it('.filter() - get DOM elements that match the selector', () => {
// https://on.cypress.io/filter
cy.get('.traversal-nav>li')
.filter('.active').should('contain', 'About')
})
it('.find() - get descendant DOM elements of the selector', () => {
// https://on.cypress.io/find
cy.get('.traversal-pagination')
.find('li').find('a')
.should('have.length', 7)
})
it('.first() - get first DOM element', () => {
// https://on.cypress.io/first
cy.get('.traversal-table td')
.first().should('contain', '1')
})
it('.last() - get last DOM element', () => {
// https://on.cypress.io/last
cy.get('.traversal-buttons .btn')
.last().should('contain', 'Submit')
})
it('.next() - get next sibling DOM element', () => {
// https://on.cypress.io/next
cy.get('.traversal-ul')
.contains('apples').next().should('contain', 'oranges')
})
it('.nextAll() - get all next sibling DOM elements', () => {
// https://on.cypress.io/nextall
cy.get('.traversal-next-all')
.contains('oranges')
.nextAll().should('have.length', 3)
})
it('.nextUntil() - get next sibling DOM elements until next el', () => {
// https://on.cypress.io/nextuntil
cy.get('#veggies')
.nextUntil('#nuts').should('have.length', 3)
})
it('.not() - remove DOM elements from set of DOM elements', () => {
// https://on.cypress.io/not
cy.get('.traversal-disabled .btn')
.not('[disabled]').should('not.contain', 'Disabled')
})
it('.parent() - get parent DOM element from DOM elements', () => {
// https://on.cypress.io/parent
cy.get('.traversal-mark')
.parent().should('contain', 'Morbi leo risus')
})
it('.parents() - get parent DOM elements from DOM elements', () => {
// https://on.cypress.io/parents
cy.get('.traversal-cite')
.parents().should('match', 'blockquote')
})
it('.parentsUntil() - get parent DOM elements from DOM elements until el', () => {
// https://on.cypress.io/parentsuntil
cy.get('.clothes-nav')
.find('.active')
.parentsUntil('.clothes-nav')
.should('have.length', 2)
})
it('.prev() - get previous sibling DOM element', () => {
// https://on.cypress.io/prev
cy.get('.birds').find('.active')
.prev().should('contain', 'Lorikeets')
})
it('.prevAll() - get all previous sibling DOM elements', () => {
// https://on.cypress.io/prevAll
cy.get('.fruits-list').find('.third')
.prevAll().should('have.length', 2)
})
it('.prevUntil() - get all previous sibling DOM elements until el', () => {
// https://on.cypress.io/prevUntil
cy.get('.foods-list').find('#nuts')
.prevUntil('#veggies').should('have.length', 3)
})
it('.siblings() - get all sibling DOM elements', () => {
// https://on.cypress.io/siblings
cy.get('.traversal-pills .active')
.siblings().should('have.length', 2)
})
})

View File

@@ -0,0 +1,133 @@
/// <reference types="Cypress" />
context('Utilities', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/utilities')
})
it('Cypress._ - call a lodash method', () => {
// https://on.cypress.io/_
cy.request('https://jsonplaceholder.cypress.io/users')
.then((response) => {
let ids = Cypress._.chain(response.body).map('id').take(3).value()
expect(ids).to.deep.eq([1, 2, 3])
})
})
it('Cypress.$ - call a jQuery method', () => {
// https://on.cypress.io/$
let $li = Cypress.$('.utility-jquery li:first')
cy.wrap($li)
.should('not.have.class', 'active')
.click()
.should('have.class', 'active')
})
it('Cypress.Blob - blob utilities and base64 string conversion', () => {
// https://on.cypress.io/blob
cy.get('.utility-blob').then(($div) =>
// https://github.com/nolanlawson/blob-util#imgSrcToDataURL
// get the dataUrl string for the javascript-logo
Cypress.Blob.imgSrcToDataURL('https://example.cypress.io/assets/img/javascript-logo.png', undefined, 'anonymous')
.then((dataUrl) => {
// create an <img> element and set its src to the dataUrl
let img = Cypress.$('<img />', { src: dataUrl })
// need to explicitly return cy here since we are initially returning
// the Cypress.Blob.imgSrcToDataURL promise to our test
// append the image
$div.append(img)
cy.get('.utility-blob img').click()
.should('have.attr', 'src', dataUrl)
}))
})
it('Cypress.minimatch - test out glob patterns against strings', () => {
// https://on.cypress.io/minimatch
let matching = Cypress.minimatch('/users/1/comments', '/users/*/comments', {
matchBase: true,
})
expect(matching, 'matching wildcard').to.be.true
matching = Cypress.minimatch('/users/1/comments/2', '/users/*/comments', {
matchBase: true,
})
expect(matching, 'comments').to.be.false
// ** matches against all downstream path segments
matching = Cypress.minimatch('/foo/bar/baz/123/quux?a=b&c=2', '/foo/**', {
matchBase: true,
})
expect(matching, 'comments').to.be.true
// whereas * matches only the next path segment
matching = Cypress.minimatch('/foo/bar/baz/123/quux?a=b&c=2', '/foo/*', {
matchBase: false,
})
expect(matching, 'comments').to.be.false
})
it('Cypress.moment() - format or parse dates using a moment method', () => {
// https://on.cypress.io/moment
const time = Cypress.moment().utc('2014-04-25T19:38:53.196Z').format('h:mm A')
expect(time).to.be.a('string')
cy.get('.utility-moment').contains('3:38 PM')
.should('have.class', 'badge')
// the time in the element should be between 3pm and 5pm
const start = Cypress.moment('3:00 PM', 'LT')
const end = Cypress.moment('5:00 PM', 'LT')
cy.get('.utility-moment .badge')
.should(($el) => {
// parse American time like "3:38 PM"
const m = Cypress.moment($el.text().trim(), 'LT')
// display hours + minutes + AM|PM
const f = 'h:mm A'
expect(m.isBetween(start, end),
`${m.format(f)} should be between ${start.format(f)} and ${end.format(f)}`).to.be.true
})
})
it('Cypress.Promise - instantiate a bluebird promise', () => {
// https://on.cypress.io/promise
let waited = false
/**
* @return Bluebird<string>
*/
function waitOneSecond () {
// return a promise that resolves after 1 second
// @ts-ignore TS2351 (new Cypress.Promise)
return new Cypress.Promise((resolve, reject) => {
setTimeout(() => {
// set waited to true
waited = true
// resolve with 'foo' string
resolve('foo')
}, 1000)
})
}
cy.then(() =>
// return a promise to cy.then() that
// is awaited until it resolves
// @ts-ignore TS7006
waitOneSecond().then((str) => {
expect(str).to.eq('foo')
expect(waited).to.be.true
}))
})
})

View File

@@ -0,0 +1,59 @@
/// <reference types="Cypress" />
context('Viewport', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/viewport')
})
it('cy.viewport() - set the viewport size and dimension', () => {
// https://on.cypress.io/viewport
cy.get('#navbar').should('be.visible')
cy.viewport(320, 480)
// the navbar should have collapse since our screen is smaller
cy.get('#navbar').should('not.be.visible')
cy.get('.navbar-toggle').should('be.visible').click()
cy.get('.nav').find('a').should('be.visible')
// lets see what our app looks like on a super large screen
cy.viewport(2999, 2999)
// cy.viewport() accepts a set of preset sizes
// to easily set the screen to a device's width and height
// We added a cy.wait() between each viewport change so you can see
// the change otherwise it is a little too fast to see :)
cy.viewport('macbook-15')
cy.wait(200)
cy.viewport('macbook-13')
cy.wait(200)
cy.viewport('macbook-11')
cy.wait(200)
cy.viewport('ipad-2')
cy.wait(200)
cy.viewport('ipad-mini')
cy.wait(200)
cy.viewport('iphone-6+')
cy.wait(200)
cy.viewport('iphone-6')
cy.wait(200)
cy.viewport('iphone-5')
cy.wait(200)
cy.viewport('iphone-4')
cy.wait(200)
cy.viewport('iphone-3')
cy.wait(200)
// cy.viewport() accepts an orientation for all presets
// the default orientation is 'portrait'
cy.viewport('ipad-2', 'portrait')
cy.wait(200)
cy.viewport('iphone-4', 'landscape')
cy.wait(200)
// The viewport will be reset back to the default dimensions
// in between tests (the default can be set in cypress.json)
})
})

View File

@@ -0,0 +1,34 @@
/// <reference types="Cypress" />
context('Waiting', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/waiting')
})
// BE CAREFUL of adding unnecessary wait times.
// https://on.cypress.io/best-practices#Unnecessary-Waiting
// https://on.cypress.io/wait
it('cy.wait() - wait for a specific amount of time', () => {
cy.get('.wait-input1').type('Wait 1000ms after typing')
cy.wait(1000)
cy.get('.wait-input2').type('Wait 1000ms after typing')
cy.wait(1000)
cy.get('.wait-input3').type('Wait 1000ms after typing')
cy.wait(1000)
})
it('cy.wait() - wait for a specific route', () => {
cy.server()
// Listen to GET to comments/1
cy.route('GET', 'comments/*').as('getComment')
// we have code that gets a comment when
// the button is clicked in scripts.js
cy.get('.network-btn').click()
// wait for GET comments/1
cy.wait('@getComment').its('status').should('eq', 200)
})
})

View File

@@ -0,0 +1,22 @@
/// <reference types="Cypress" />
context('Window', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/window')
})
it('cy.window() - get the global window object', () => {
// https://on.cypress.io/window
cy.window().should('have.property', 'top')
})
it('cy.document() - get the document object', () => {
// https://on.cypress.io/document
cy.document().should('have.property', 'charset').and('eq', 'UTF-8')
})
it('cy.title() - get the title', () => {
// https://on.cypress.io/title
cy.title().should('include', 'Kitchen Sink')
})
})

View File

@@ -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"
}

28
cypress/helpers/util.js Normal file
View File

@@ -0,0 +1,28 @@
/* eslint-env jest */
import { Base64 } from 'js-base64';
export const mermaidUrl = (graphStr, options, api) => {
const obj = {
code: graphStr,
mermaid: options
};
const objStr = JSON.stringify(obj);
let url = 'http://localhost:9000/e2e.html?graph=' + Base64.encodeURI(objStr);
if (api) {
url = 'http://localhost:9000/xss.html?graph=' + graphStr;
}
if (options.listUrl) {
cy.log(options.listId, ' ', url);
}
return url;
};
export const imgSnapshotTest = (graphStr, options, api) => {
const url = mermaidUrl(graphStr, options, api);
cy.visit(url);
cy.get('svg');
cy.percySnapshot();
};

View File

@@ -0,0 +1,242 @@
/* eslint-env jest */
describe('Interaction', () => {
describe('Interaction - security level loose', () => {
it('should handle a click on a node with a bound function', () => {
const url = 'http://localhost:9000/click_security_loose.html';
cy.viewport(1440, 1024);
cy.visit(url);
cy.get('body')
.find('g#Function')
.click();
cy.get('.created-by-click').should('have.text', 'Clicked By Flow');
});
it('should handle a click on a node with a bound function where the node starts with a number', () => {
const url = 'http://localhost:9000/click_security_loose.html';
cy.viewport(1440, 1024);
cy.visit(url);
cy.get('body')
.find('g#s1Function')
.click();
cy.get('.created-by-click').should('have.text', 'Clicked By Flow');
});
it('should handle a click on a node with a bound url', () => {
const url = 'http://localhost:9000/click_security_loose.html';
cy.viewport(1440, 1024);
cy.visit(url);
cy.get('body')
.find('g#URL')
.click();
cy.location().should(location => {
expect(location.href).to.eq('http://localhost:9000/webpackUsage.html');
});
});
it('should handle a click on a node with a bound url where the node starts with a number', () => {
const url = 'http://localhost:9000/click_security_loose.html';
cy.viewport(1440, 1024);
cy.visit(url);
cy.get('body')
.find('g#s2URL')
.click();
cy.location().should(location => {
expect(location.href).to.eq('http://localhost:9000/webpackUsage.html');
});
});
it('should handle a click on a task with a bound URL clicking on the rect', () => {
const url = 'http://localhost:9000/click_security_loose.html';
cy.viewport(1440, 1024);
cy.visit(url);
cy.get('body')
.find('rect#cl1')
.click({ force: true });
cy.location().should(location => {
expect(location.href).to.eq('http://localhost:9000/webpackUsage.html');
});
});
it('should handle a click on a task with a bound URL clicking on the text', () => {
const url = 'http://localhost:9000/click_security_loose.html';
cy.viewport(1440, 1024);
cy.visit(url);
cy.get('body')
.find('text#cl1-text')
.click({ force: true });
cy.location().should(location => {
expect(location.href).to.eq('http://localhost:9000/webpackUsage.html');
});
});
it('should handle a click on a task with a bound function', () => {
const url = 'http://localhost:9000/click_security_loose.html';
cy.viewport(1440, 1024);
cy.visit(url);
cy.get('body')
.find('rect#cl2')
.click({ force: true });
cy.get('.created-by-gant-click').should('have.text', 'Clicked By Gant');
});
it('should handle a click on a task with a bound function', () => {
const url = 'http://localhost:9000/click_security_loose.html';
cy.viewport(1440, 1024);
cy.visit(url);
cy.get('body')
.find('text#cl2-text')
.click({ force: true });
cy.get('.created-by-gant-click').should('have.text', 'Clicked By Gant');
});
});
describe('Interaction - security level tight', () => {
it('should handle a click on a node without a bound function', () => {
const url = 'http://localhost:9000/click_security_strict.html';
cy.viewport(1440, 1024);
cy.visit(url);
cy.get('body')
.find('g#Function')
.click();
cy.get('.created-by-click').should('not.have.text', 'Clicked By Flow');
});
it('should handle a click on a node with a bound function where the node starts with a number', () => {
const url = 'http://localhost:9000/click_security_strict.html';
cy.viewport(1440, 1024);
cy.visit(url);
cy.get('body')
.find('g#s1Function')
.click();
cy.get('.created-by-click').should('not.have.text', 'Clicked By Flow');
});
it('should handle a click on a node with a bound url', () => {
const url = 'http://localhost:9000/click_security_strict.html';
cy.viewport(1440, 1024);
cy.visit(url);
cy.get('body')
.find('g#URL')
.click();
cy.location().should(location => {
expect(location.href).to.eq('http://localhost:9000/webpackUsage.html');
});
});
it('should handle a click on a node with a bound url where the node starts with a number', () => {
const url = 'http://localhost:9000/click_security_strict.html';
cy.viewport(1440, 1024);
cy.visit(url);
cy.get('body')
.find('g#s2URL')
.click();
cy.location().should(location => {
expect(location.href).to.eq('http://localhost:9000/webpackUsage.html');
});
});
it('should handle a click on a task with a bound URL clicking on the rect', () => {
const url = 'http://localhost:9000/click_security_strict.html';
cy.viewport(1440, 1024);
cy.visit(url);
cy.get('body')
.find('rect#cl1')
.click({ force: true });
cy.location().should(location => {
expect(location.href).to.eq('http://localhost:9000/webpackUsage.html');
});
});
it('should handle a click on a task with a bound URL clicking on the text', () => {
const url = 'http://localhost:9000/click_security_strict.html';
cy.viewport(1440, 1024);
cy.visit(url);
cy.get('body')
.find('text#cl1-text')
.click({ force: true });
cy.location().should(location => {
expect(location.href).to.eq('http://localhost:9000/webpackUsage.html');
});
});
it('should handle a click on a task with a bound function', () => {
const url = 'http://localhost:9000/click_security_strict.html';
cy.viewport(1440, 1024);
cy.visit(url);
cy.get('body')
.find('rect#cl2')
.click({ force: true });
cy.get('.created-by-gant-click').should('not.have.text', 'Clicked By Gant');
});
it('should handle a click on a task with a bound function', () => {
const url = 'http://localhost:9000/click_security_strict.html';
cy.viewport(1440, 1024);
cy.visit(url);
cy.get('body')
.find('text#cl2-text')
.click({ force: true });
cy.get('.created-by-gant-click').should('not.have.text', 'Clicked By Gant');
});
});
describe('Interaction - security level other, missspelling', () => {
it('should handle a click on a node with a bound function', () => {
const url = 'http://localhost:9000/click_security_strict.html';
cy.viewport(1440, 1024);
cy.visit(url);
cy.get('body')
.find('g#Function')
.click();
cy.get('.created-by-click').should('not.have.text', 'Clicked By Flow');
});
it('should handle a click on a node with a bound function where the node starts with a number', () => {
const url = 'http://localhost:9000/click_security_strict.html';
cy.viewport(1440, 1024);
cy.visit(url);
cy.get('body')
.find('g#s1Function')
.click();
cy.get('.created-by-click').should('not.have.text', 'Clicked By Flow');
});
it('should handle a click on a node with a bound url', () => {
const url = 'http://localhost:9000/click_security_strict.html';
cy.viewport(1440, 1024);
cy.visit(url);
cy.get('body')
.find('g#URL')
.click();
cy.location().should(location => {
expect(location.href).to.eq('http://localhost:9000/webpackUsage.html');
});
});
it('should handle a click on a task with a bound function', () => {
const url = 'http://localhost:9000/click_security_strict.html';
cy.viewport(1440, 1024);
cy.visit(url);
cy.get('body')
.find('rect#cl2')
.click({ force: true });
cy.get('.created-by-gant-click').should('not.have.text', 'Clicked By Gant');
});
it('should handle a click on a task with a bound function', () => {
const url = 'http://localhost:9000/click_security_strict.html';
cy.viewport(1440, 1024);
cy.visit(url);
cy.get('body')
.find('text#cl2-text')
.click({ force: true });
cy.get('.created-by-gant-click').should('not.have.text', 'Clicked By Gant');
});
});
});

View File

@@ -0,0 +1,11 @@
/* eslint-env jest */
describe('Sequencediagram', () => {
it('should render a simple sequence diagrams', () => {
const url = 'http://localhost:9000/webpackUsage.html';
cy.visit(url);
cy.get('body')
.find('svg')
.should('have.length', 2);
});
});

View File

@@ -0,0 +1,16 @@
/* eslint-env jest */
import { mermaidUrl } from '../../helpers/util.js';
/* eslint-disable */
describe('XSS', () => {
it('should handle xss in tags', () => {
const str = 'eyJjb2RlIjoiXG5ncmFwaCBMUlxuICAgICAgQi0tPkQoPGltZyBvbmVycm9yPWxvY2F0aW9uPWBqYXZhc2NyaXB0XFx1MDAzYXhzc0F0dGFja1xcdTAwMjhkb2N1bWVudC5kb21haW5cXHUwMDI5YCBzcmM9eD4pOyIsIm1lcm1haWQiOnsidGhlbWUiOiJkZWZhdWx0In19';
const url = mermaidUrl(str,{}, true);
cy.visit(url);
cy.get('svg')
cy.percySnapshot()
})
})

View File

@@ -1,12 +1,10 @@
/* eslint-env jest */
import { imgSnapshotTest } from '../helpers/util.js'
const { toMatchImageSnapshot } = require('jest-image-snapshot')
expect.extend({ toMatchImageSnapshot })
import { imgSnapshotTest } from '../../helpers/util';
describe('Sequencediagram', () => {
it('should render a simple class diagrams', async () => {
await imgSnapshotTest(page, `
it('should render a simple class diagrams', () => {
imgSnapshotTest(
`
classDiagram
Class01 <|-- AveryLongClass : Cool
Class03 *-- Class04
@@ -22,6 +20,8 @@ describe('Sequencediagram', () => {
Class01 : int gorilla
Class08 <--> C2: Cool label
`,
{})
})
})
{}
);
cy.get('svg');
});
});

View File

@@ -1,22 +1,22 @@
/* eslint-env jest */
import { imgSnapshotTest } from '../helpers/util.js'
const { toMatchImageSnapshot } = require('jest-image-snapshot')
expect.extend({ toMatchImageSnapshot })
import { imgSnapshotTest } from '../../helpers/util';
describe('Flowcart', () => {
it('should render a simple flowchart', async () => {
await imgSnapshotTest(page, `graph TD
it('should render a simple flowchart', () => {
imgSnapshotTest(
`graph TD
A[Christmas] -->|Get money| B(Go shopping)
B --> C{Let me think}
C -->|One| D[Laptop]
C -->|Two| E[iPhone]
C -->|Three| F[fa:fa-car Car]
`,
{})
})
it('should render a simple flowchart with line breaks', async () => {
await imgSnapshotTest(page, `
{}
);
});
it('should render a simple flowchart with line breaks', () => {
imgSnapshotTest(
`
graph TD
A[Christmas] -->|Get money| B(Go shopping)
B --> C{Let me thinksssss<br/>ssssssssssssssssssssss<br/>sssssssssssssssssssssssssss}
@@ -24,11 +24,13 @@ describe('Flowcart', () => {
C -->|Two| E[iPhone]
C -->|Three| F[Car]
`,
{})
})
{}
);
});
it('should render a simple flowchart with trapezoid and inverse trapezoid vertex options.', async () => {
await imgSnapshotTest(page, `
it('should render a simple flowchart with trapezoid and inverse trapezoid vertex options.', () => {
imgSnapshotTest(
`
graph TD
A[/Christmas\\]
A -->|Get money| B[\\Go shopping/]
@@ -37,11 +39,29 @@ describe('Flowcart', () => {
C -->|Two| E[\\iPhone\\]
C -->|Three| F[Car]
`,
{})
})
{}
);
});
it('should render a flowchart full of circles', async () => {
await imgSnapshotTest(page, `
it('should style nodes via a class.', () => {
imgSnapshotTest(
`
graph TD
1A --> 1B
1B --> 1C
1C --> D
1C --> E
classDef processHead fill:#888888,color:white,font-weight:bold,stroke-width:3px,stroke:#001f3f
class 1A,1B,D,E processHead
`,
{}
);
});
it('should render a flowchart full of circles', () => {
imgSnapshotTest(
`
graph LR
47(SAM.CommonFA.FMESummary)-->48(SAM.CommonFA.CommonFAFinanceBudget)
37(SAM.CommonFA.BudgetSubserviceLineVolume)-->48(SAM.CommonFA.CommonFAFinanceBudget)
@@ -64,10 +84,12 @@ describe('Flowcart', () => {
35(SAM.CommonFA.PopulationFME)-->39(SAM.CommonFA.ChargeDetails)
36(SAM.CommonFA.PremetricCost)-->39(SAM.CommonFA.ChargeDetails)
`,
{})
})
it('should render a flowchart full of icons', async () => {
await imgSnapshotTest(page, `
{}
);
});
it('should render a flowchart full of icons', () => {
imgSnapshotTest(
`
graph TD
9e122290_1ec3_e711_8c5a_005056ad0002("fa:fa-creative-commons My System | Test Environment")
82072290_1ec3_e711_8c5a_005056ad0002("fa:fa-cogs Shared Business Logic Server:Service 1")
@@ -132,21 +154,45 @@ describe('Flowcart', () => {
9a072290_1ec3_e711_8c5a_005056ad0002-->d6072290_1ec3_e711_8c5a_005056ad0002
9a072290_1ec3_e711_8c5a_005056ad0002-->71082290_1ec3_e711_8c5a_005056ad0002
`,
{})
})
{}
);
});
it('should render subgraphs', async () => {
await imgSnapshotTest(page, `
it('should render labels with numbers at the start', () => {
imgSnapshotTest(
`
graph TB;subgraph "number as labels";1;end;
`,
{}
);
});
it('should render subgraphs', () => {
imgSnapshotTest(
`
graph TB
subgraph One
a1-->a2
end
`,
{})
})
{}
);
});
it('should render styled subgraphs', async () => {
await imgSnapshotTest(page, `
it('should render subgraphs with a title startign with a digit', () => {
imgSnapshotTest(
`
graph TB
subgraph 2Two
a1-->a2
end
`,
{}
);
});
it('should render styled subgraphs', () => {
imgSnapshotTest(
`
graph TB
A
B
@@ -175,11 +221,13 @@ describe('Flowcart', () => {
style foo fill:#F99,stroke-width:2px,stroke:#F0F
style bar fill:#999,stroke-width:10px,stroke:#0F0
`,
{})
})
{}
);
});
it('should render a flowchart with ling sames and class definitoins', async () => {
await imgSnapshotTest(page, `graph LR
it('should render a flowchart with ling sames and class definitoins', () => {
imgSnapshotTest(
`graph LR
sid-B3655226-6C29-4D00-B685-3D5C734DC7E1["
提交申请
@@ -275,6 +323,25 @@ describe('Flowcart', () => {
sid-7CE72B24-E0C1-46D3-8132-8BA66BE05AA7-->sid-4DA958A0-26D9-4D47-93A7-70F39FD7D51A;
sid-7CE72B24-E0C1-46D3-8132-8BA66BE05AA7-->sid-4FC27B48-A6F9-460A-A675-021F5854FE22;
`,
{})
})
})
{}
);
});
it('should render color of styled nodes', () => {
imgSnapshotTest(
`
graph LR
foo-->bar
classDef foo fill:lightblue,color:green,stroke:#FF9E2C,font-weight:bold
style foo fill:#F99,stroke-width:2px,stroke:#F0F
style bar fill:#999,color: #00ff00, stroke-width:10px,stroke:#0F0
`,
{
listUrl: false,
listId: 'color styling',
logLevel: 0
}
);
});
});

View File

@@ -1,12 +1,10 @@
/* eslint-env jest */
import { imgSnapshotTest } from '../helpers/util.js'
const { toMatchImageSnapshot } = require('jest-image-snapshot')
expect.extend({ toMatchImageSnapshot })
import { imgSnapshotTest } from '../../helpers/util.js';
describe('Sequencediagram', () => {
it('should render a gantt chart', async () => {
await imgSnapshotTest(page, `
it('should render a gantt chart', () => {
imgSnapshotTest(
`
gantt
dateFormat YYYY-MM-DD
axisFormat %d/%m
@@ -37,6 +35,7 @@ describe('Sequencediagram', () => {
Add gantt diagram to demo page : 20h
Add another diagram to demo page : 48h
`,
{})
})
})
{}
);
});
});

View File

@@ -1,12 +1,10 @@
/* eslint-env jest */
import { imgSnapshotTest } from '../helpers/util.js'
const { toMatchImageSnapshot } = require('jest-image-snapshot')
expect.extend({ toMatchImageSnapshot })
import { imgSnapshotTest } from '../../helpers/util.js';
describe('Sequencediagram', () => {
it('should render a simple git graph', async () => {
await imgSnapshotTest(page, `
it('should render a simple git graph', () => {
imgSnapshotTest(
`
gitGraph:
options
{
@@ -24,6 +22,7 @@ describe('Sequencediagram', () => {
commit
merge newbranch
`,
{})
})
})
{}
);
});
});

View File

@@ -0,0 +1,14 @@
/* eslint-env jest */
import { imgSnapshotTest } from '../../helpers/util.js';
describe('Sequencediagram', () => {
it('should render a simple info diagrams', () => {
imgSnapshotTest(
`
info
showInfo
`,
{}
);
});
});

View File

@@ -0,0 +1,16 @@
/* eslint-env jest */
import { imgSnapshotTest } from '../../helpers/util.js';
describe('Pie Chart', () => {
it('should render a simple pie diagram', () => {
imgSnapshotTest(
`
pie title Sports in Sweden
"Bandy" : 40
"Ice-Hockey" : 80
"Football" : 90
`,
{}
);
});
});

View File

@@ -1,12 +1,11 @@
/* eslint-env jest */
import { imgSnapshotTest } from '../helpers/util.js'
const { toMatchImageSnapshot } = require('jest-image-snapshot')
/// <reference types="Cypress" />
expect.extend({ toMatchImageSnapshot })
import { imgSnapshotTest } from '../../helpers/util';
describe('Sequencediagram', () => {
it('should render a simple sequence diagrams', async () => {
await imgSnapshotTest(page, `
context('Aliasing', () => {
it('should render a simple sequence diagrams', () => {
imgSnapshotTest(
`
sequenceDiagram
participant Alice
participant Bob
@@ -30,11 +29,13 @@ describe('Sequencediagram', () => {
Alice -->> John: Parallel message 2
end
`,
{})
})
describe('background rects', async () => {
it('should render a single and nested rects', async () => {
await imgSnapshotTest(page, `
{}
);
});
context('background rects', () => {
it('should render a single and nested rects', () => {
imgSnapshotTest(
`
sequenceDiagram
participant A
participant B
@@ -48,7 +49,7 @@ describe('Sequencediagram', () => {
B ->>+ C: Task 2
C -->>- B: Return
end
A ->> D: Task 3
rect rgb(0, 128, 255)
D ->>+ E: Task 4
@@ -59,10 +60,13 @@ describe('Sequencediagram', () => {
E ->> E: Task 6
end
D -->> A: Complete
`, {})
})
it('should render rect around and inside loops', async () => {
await imgSnapshotTest(page, `
`,
{}
);
});
it('should render rect around and inside loops', () => {
imgSnapshotTest(
`
sequenceDiagram
A ->> B: 1
rect rgb(204, 0, 102)
@@ -78,10 +82,13 @@ describe('Sequencediagram', () => {
D --> C: 4
end
end
`, {})
})
it('should render rect around and inside alts', async () => {
await imgSnapshotTest(page, `
`,
{}
);
});
it('should render rect around and inside alts', () => {
imgSnapshotTest(
`
sequenceDiagram
A ->> B: 1
rect rgb(204, 0, 102)
@@ -94,10 +101,13 @@ describe('Sequencediagram', () => {
end
end
B ->> A: Return
`, {})
})
it('should render rect around and inside opts', async () => {
await imgSnapshotTest(page, `
`,
{}
);
});
it('should render rect around and inside opts', () => {
imgSnapshotTest(
`
sequenceDiagram
A ->> B: 1
rect rgb(204, 0, 102)
@@ -115,7 +125,9 @@ describe('Sequencediagram', () => {
end
end
B ->> A: Return
`, {})
})
})
})
`,
{}
);
});
});
});

View File

@@ -11,7 +11,13 @@
graph TB
Function-->URL
click Function clickByFlow "Add a div"
click URL "https://mermaidjs.github.io/" "Visit <strong>mermaid docs</strong>"
click URL "http://localhost:9000/webpackUsage.html" "Visit <strong>mermaid docs</strong>"
</div>
<div id="FirstLine" class="mermaid">
graph TB
1Function-->2URL
click 1Function clickByFlow "Add a div"
click 2URL "http://localhost:9000/webpackUsage.html" "Visit <strong>mermaid docs</strong>"
</div>
<div class="mermaid">
@@ -44,7 +50,7 @@
Visit mermaidjs :active, cl1, 2014-01-07,2014-01-10
Calling a Callback (look at the console log) :cl2, after cl1, 3d
click cl1 href "https://mermaidjs.github.io/"
click cl1 href "http://localhost:9000/webpackUsage.html"
click cl2 call clickByGantt("test", test, test)
section Last section

View File

@@ -0,0 +1,83 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Mermaid Quick Test Page</title>
<link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgo=">
</head>
<body>
<div id="FirstLine" class="mermaid">
graph TB
Function-->URL
click Function clickByFlow "Add a div"
click URL "http://localhost:9000/webpackUsage.html" "Visit <strong>mermaid docs</strong>"
</div>
<div id="FirstLine" class="mermaid">
graph TB
1Function-->2URL
click 1Function clickByFlow "Add a div"
click 2URL "http://localhost:9000/webpackUsage.html" "Visit <strong>mermaid docs</strong>"
</div>
<div class="mermaid">
gantt
dateFormat YYYY-MM-DD
axisFormat %d/%m
title Adding GANTT diagram to mermaid
excludes weekdays 2014-01-10
section A section
Completed task :done, des1, 2014-01-06,2014-01-08
Active task :active, des2, 2014-01-09, 3d
Future task : des3, after des2, 5d
Future task2 : des4, after des3, 5d
section Critical tasks
Completed task in the critical line :crit, done, 2014-01-06,24h
Implement parser and jison :crit, done, after des1, 2d
Create tests for parser :crit, active, 3d
Future task in critical line :crit, 5d
Create tests for renderer :2d
Add to mermaid :1d
section Documentation
Describe gantt syntax :active, a1, after des1, 3d
Add gantt diagram to demo page :after a1 , 20h
Add another diagram to demo page :doc1, after a1 , 48h
section Clickable
Visit mermaidjs :active, cl1, 2014-01-07,2014-01-10
Calling a Callback (look at the console log) :cl2, after cl1, 3d
click cl1 href "http://localhost:9000/webpackUsage.html"
click cl2 call clickByGantt("test", test, test)
section Last section
Describe gantt syntax :after doc1, 3d
Add gantt diagram to demo page : 20h
Add another diagram to demo page : 48h
</div>
<script src="./mermaid.js"></script>
<script>
function clickByFlow(elemName) {
const div = document.createElement('div')
div.className = 'created-by-click'
div.style = 'padding: 20px; background: green; color: white;'
div.innerText = 'Clicked By Flow'
document.getElementsByTagName('body')[0].appendChild(div)
}
function clickByGantt(elemName) {
const div = document.createElement('div')
div.className = 'created-by-gant-click'
div.style = 'padding: 20px; background: green; color: white;'
div.innerText = 'Clicked By Gant'
document.getElementsByTagName('body')[0].appendChild(div)
}
mermaid.initialize({ startOnLoad: true, securityLevel: 'strct', logLevel: 1 });
</script>
</body>
</html>

View File

@@ -0,0 +1,83 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Mermaid Quick Test Page</title>
<link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgo=">
</head>
<body>
<div id="FirstLine" class="mermaid">
graph TB
Function-->URL
click Function clickByFlow "Add a div"
click URL "http://localhost:9000/webpackUsage.html" "Visit <strong>mermaid docs</strong>"
</div>
<div id="FirstLine" class="mermaid">
graph TB
1Function-->2URL
click 1Function clickByFlow "Add a div"
click 2URL "http://localhost:9000/webpackUsage.html" "Visit <strong>mermaid docs</strong>"
</div>
<div class="mermaid">
gantt
dateFormat YYYY-MM-DD
axisFormat %d/%m
title Adding GANTT diagram to mermaid
excludes weekdays 2014-01-10
section A section
Completed task :done, des1, 2014-01-06,2014-01-08
Active task :active, des2, 2014-01-09, 3d
Future task : des3, after des2, 5d
Future task2 : des4, after des3, 5d
section Critical tasks
Completed task in the critical line :crit, done, 2014-01-06,24h
Implement parser and jison :crit, done, after des1, 2d
Create tests for parser :crit, active, 3d
Future task in critical line :crit, 5d
Create tests for renderer :2d
Add to mermaid :1d
section Documentation
Describe gantt syntax :active, a1, after des1, 3d
Add gantt diagram to demo page :after a1 , 20h
Add another diagram to demo page :doc1, after a1 , 48h
section Clickable
Visit mermaidjs :active, cl1, 2014-01-07,2014-01-10
Calling a Callback (look at the console log) :cl2, after cl1, 3d
click cl1 href "http://localhost:9000/webpackUsage.html"
click cl2 call clickByGantt("test", test, test)
section Last section
Describe gantt syntax :after doc1, 3d
Add gantt diagram to demo page : 20h
Add another diagram to demo page : 48h
</div>
<script src="./mermaid.js"></script>
<script>
function clickByFlow(elemName) {
const div = document.createElement('div')
div.className = 'created-by-click'
div.style = 'padding: 20px; background: green; color: white;'
div.innerText = 'Clicked By Flow'
document.getElementsByTagName('body')[0].appendChild(div)
}
function clickByGantt(elemName) {
const div = document.createElement('div')
div.className = 'created-by-gant-click'
div.style = 'padding: 20px; background: green; color: white;'
div.innerText = 'Clicked By Gant'
document.getElementsByTagName('body')[0].appendChild(div)
}
mermaid.initialize({ startOnLoad: true, securityLevel: 'strict', logLevel: 1 });
</script>
</body>
</html>

View File

@@ -10,6 +10,8 @@
<body>
<script src="./mermaid.js"></script>
<script>
// Notice startOnLoad=false
// This prevents default handling in mermaid from render before the e2e logic is applied
mermaid.initialize({
startOnLoad: false,
useMaxWidth: true,

View File

@@ -0,0 +1,46 @@
<html>
<head>
<link
href="https://fonts.googleapis.com/css?family=Montserrat&display=swap"
rel="stylesheet"
/>
<style>body {
font-family: 'trebuchet ms', verdana, arial;
}</style>
</head>
<body>
<div class="mermaid">
graph TB
subgraph One
a1-->a2-->a3
end
</div>
<div class="mermaid">
graph TB
a_a --> b_b:::apa --> c_c:::apa
classDef apa fill:#f9f,stroke:#333,stroke-width:4px;
class a_a apa;
</div>
<div class="mermaid">
graph TB
a_a(Aftonbladet) --> b_b[gorilla]:::apa --> c_c{chimp}:::apa -->a_a
a_a --> c --> d_d --> c_c
classDef apa fill:#f9f,stroke:#333,stroke-width:4px;
class a_a apa;
click a_a "http://www.aftonbladet.se" "apa"
</div>
<script src="./mermaid.js"></script>
<script>
mermaid.initialize({
theme: 'forest',
// themeCSS: '.node rect { fill: red; }',
logLevel: 3,
flowchart: { curve: 'linear' },
gantt: { axisFormat: '%m/%d/%Y' },
sequence: { actorMargin: 50 },
// sequenceDiagram: { actorMargin: 300 } // deprecated
});
</script>
</script>
</body>
</html>

View File

@@ -0,0 +1,37 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Mermaid Quick Test Page</title>
<link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgo=">
<style>
body {
font-family: 'trebuchet ms', verdana, arial;
}
</style>
</head>
<body>
<div class="mermaid">
info
</div>
<div class="mermaid">
graph TD
subgraph one
1
end
</div>
<!-- <div class="mermaid">
graph TD
A --> B --> C
</div> -->
<script src="./mermaid.js"></script>
<script>
function showFullFirstSquad(elemName) {
console.log('show ' + elemName);
}
mermaid.initialize({ startOnLoad: true, securityLevel: 'loose', logLevel: 1 });
</script>
</body>
</html>

View File

@@ -20,6 +20,7 @@ const contentLoaded = function () {
div.innerHTML = graphObj.code
document.getElementsByTagName('body')[0].appendChild(div)
global.mermaid.initialize(graphObj.mermaid)
// console.log('graphObj.mermaid', graphObj.mermaid)
global.mermaid.init()
}
}
@@ -30,7 +31,6 @@ const contentLoadedApi = function () {
const graphBase64 = document.location.href.substr(pos)
const graphObj = JSON.parse(Base64.decode(graphBase64))
// const graph = 'hello'
console.log(graphObj)
const div = document.createElement('div')
div.id = 'block'
div.className = 'mermaid'
@@ -57,6 +57,7 @@ if (typeof document !== 'undefined') {
this.console.log('Using api')
contentLoadedApi()
} else {
this.console.log('Not using api')
contentLoaded()
}
},

View File

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

25
cypress/plugins/index.js Normal file
View File

@@ -0,0 +1,25 @@
// ***********************************************************
// 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
// }
let percyHealthCheck = require("@percy/cypress/task");
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
on("task", percyHealthCheck);
};

View File

@@ -0,0 +1,27 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This is will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
import '@percy/cypress'

20
cypress/support/index.js Normal file
View File

@@ -0,0 +1,20 @@
// ***********************************************************
// 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')

View File

@@ -11,7 +11,7 @@
- [Flowchart](flowchart.md)
- [Sequence diagram](sequenceDiagram.md)
- [Gantt](gantt.md)
- [Pie Chart](pie.md)
- Guide
- [Development](development.md)

View File

@@ -304,7 +304,7 @@ graph TB
## Interaction
It is possible to bind a click event to a node, the click can lead to either a javascript callback or to a link which will be opened in a new browser tab. **Note**: This functionality is disabled when using securityLevel='strict'
It is possible to bind a click event to a node, the click can lead to either a javascript callback or to a link which will be opened in a new browser tab. **Note**: This functionality is disabled when using `securityLevel='strict'` and enabled when using `securityLevel='loose'`.
```
click nodeId callback
@@ -340,6 +340,34 @@ graph LR;
```
> **Success** The tooltip functionality and the ability to link to urls are available from version 0.5.2.
Beginners tip, a full example using interactive links in a html context:
```
<body>
<div class="mermaid">
graph LR;
A-->B;
click A callback "Tooltip"
click B "http://www.github.com" "This is a link"
</div>
<script>
var callback = function(){
alert('A callback was triggered');
}
var config = {
startOnLoad:true,
flowchart:{
useMaxWidth:true,
htmlLabels:true,
curve:'cardinal',
},
securityLevel:'loose',
};
mermaid.initialize(config);
</script>
</body>
```
## Styling and classes

View File

@@ -2,32 +2,66 @@
## mermaidAPI
This is the api to be used when handling the integration with the web page instead of using the default integration
(mermaid.js).
This is the api to be used when optionally handling the integration with the web page, instead of using the default integration provided by mermaid.js.
The core of this api is the **render** function that given a graph definitionas text renders the graph/diagram and
returns a svg element for the graph. It is is then up to the user of the API to make use of the svg, either insert it
somewhere in the page or something completely different.
The core of this api is the [**render**][1] function which, given a graph
definition as text, renders the graph/diagram and returns an svg element for the graph.
It is is then up to the user of the API to make use of the svg, either insert it somewhere in the page or do something completely different.
In addition to the render function, a number of behavioral configuration options are available.
## Configuration
These are the default options which can be overridden with the initialization call as in the example below:
These are the default options which can be overridden with the initialization call like so:
**Example 1:**
mermaid.initialize({
flowchart:{
htmlLabels: false
}
});
<pre>
mermaid.initialize({
flowchart:{
htmlLabels: false
}
});
</pre>
**Example 2:**
<pre>
<script>
var config = {
startOnLoad:true,
flowchart:{
useMaxWidth:true,
htmlLabels:true,
curve:'cardinal',
},
securityLevel:'loose',
};
mermaid.initialize(config);
</script>
</pre>
A summary of all options and their defaults is found [here](https://github.com/knsv/mermaid/blob/master/docs/mermaidAPI.md#mermaidapi-configuration-defaults). A description of each option follows below.
## theme
theme , the CSS style sheet
**theme** - Choose one of the built-in themes: default, forest, dark or neutral. To disable any pre-defined mermaid theme, use "null".
**theme** - Choose one of the built-in themes:
- default
- forest
- dark
- neutral.
To disable any pre-defined mermaid theme, use "null".
**themeCSS** - Use your own CSS. This overrides **theme**.
"theme": "forest",
"themeCSS": ".node rect { fill: red; }"
<pre>
"theme": "forest",
"themeCSS": ".node rect { fill: red; }"
</pre>
## logLevel
@@ -43,8 +77,8 @@ This option decides the amount of logging to be used.
Sets the level of trust to be used on the parsed diagrams.
- **true**: (**default**) tags in text are encoded, click functionality is disabeled
- **false**: tags in text are allowed, click functionality is enabled
- **strict**: (**default**) tags in text are encoded, click functionality is disabeled
- **loose**: tags in text are allowed, click functionality is enabled
## startOnLoad
@@ -69,7 +103,11 @@ on the edges.
### curve
How mermaid renders curves for flowcharts. Possibel values are basis, linear and cardinal. **Default value linear**.
How mermaid renders curves for flowcharts. Possible values are
- basis
- linear **default**
- cardinal
## sequence
@@ -77,7 +115,7 @@ The object containing configurations specific for sequence diagrams
### diagramMarginX
margin to the right and left of the sequence diagram
margin to the right and left of the sequence diagram.
**Default value 50**.
### diagramMarginY
@@ -198,7 +236,7 @@ The number of alternating section styles.
### axisFormat
Datetime format of the axis, this might need adjustment to match your locale and preferences
Datetime format of the axis. This might need adjustment to match your locale and preferences
**Default value '%Y-%m-%d'**.
## render
@@ -226,3 +264,57 @@ mermaidAPI.initialize({
- `container` selector to element in which a div with the graph temporarily will be inserted. In one is
provided a hidden div will be inserted in the body of the page instead. The element will be removed when rendering is
completed.
##
## mermaidAPI configuration defaults
<pre>
<script>
var config = {
theme:'default',
logLevel:'fatal',
securityLevel:'strict',
startOnLoad:true,
arrowMarkerAbsolute:false,
flowchart:{
htmlLabels:true,
curve:'linear',
},
sequence:{
diagramMarginX:50,
diagramMarginY:10,
actorMargin:50,
width:150,
height:65,
boxMargin:10,
boxTextMargin:5,
noteMargin:10,
messageMargin:35,
mirrorActors:true,
bottomMarginAdj:1,
useMaxWidth:true,
rightAngles:false,
showSequenceNumbers:false,
},
gantt:{
titleTopMargin:25,
barHeight:20,
barGap:4,
topPadding:50,
leftPadding:75,
gridLineStartPadding:35,
fontSize:11,
fontFamily:'"Open-Sans", "sans-serif"',
numberSectionStyles:4,
axisFormat:'%Y-%m-%d',
}
};
mermaid.initialize(config);
</script>
</pre>
[1]: https://github.com/knsv/mermaid/blob/master/docs/mermaidAPI.md#render

37
docs/pie.md Normal file
View File

@@ -0,0 +1,37 @@
# Pie chart diagrams
> A pie chart (or a circle chart) is a circular statistical graphic, which is divided into slices to illustrate numerical proportion. In a pie chart, the arc length of each slice (and consequently its central angle and area), is proportional to the quantity it represents. While it is named for its resemblance to a pie which has been sliced, there are variations on the way it can be presented. The earliest known pie chart is generally credited to William Playfair's Statistical Breviary of 1801
Mermaid can render Pie Chart diagrams.
```
pie
"Dogs" : 386
"Cats" : 85
"Rats" : 15
```
```mermaid
pie title Pets adopted by volunteers
"Dogs" : 386
"Cats" : 85
"Rats" : 35
```
## Syntax
```
pie
"DataKey1" : Positive numeric value (upto two decimal places)
"Calcium" : 42.96
"Potassium" : 50.05
"Magnesium" : 10.01
"Iron" : 5
```
```mermaid
pie title Key elements in Product X
"Calcium" : 42.96
"Potassium" : 50.05
"Magnesium" : 25.01
"Iron" : 15
```

View File

@@ -245,7 +245,7 @@ rect rgb(0, 255, 0)
end
```
```
rect rgba(0, 0, 255, .1)
rect rgba(0, 0, 255, .1)
... content ...
end
```
@@ -406,3 +406,4 @@ Param | Description | Default value
--- | --- | ---
mirrorActor | Turns on/off the rendering of actors below the diagram as well as above it | false
bottomMarginAdj | Adjusts how far down the graph ended. Wide borders styles with css could generate unwantewd clipping which is why this config param exists. | 1

View File

@@ -1,9 +0,0 @@
# End to end tests
These tests are end to end tests in the sense that they actually render a full diagram in the browser. The purpose of these tests is to simplify handling of merge requests and releases by highlighting possible unexpected side-effects.
Apart from beeing rendered in a browser the tests perform image snapshots of the diagrams. The tests is handled in the same way as regular jest snapshots tests with the difference that an image comparison is performed instead of a comparison of the generated code.
## To run the tests
1. Start the dev server by running ***yarn dev***
2. Run yarn e2e to run the tests

View File

@@ -1,30 +0,0 @@
/* eslint-env jest */
import { Base64 } from 'js-base64'
export const mermaidUrl = (graphStr, options, api) => {
const obj = {
code: graphStr,
mermaid: options
}
const objStr = JSON.stringify(obj)
let url = 'http://localhost:9000/e2e.html?graph=' + Base64.encodeURI(objStr)
if (api) {
url = 'http://localhost:9000/xss.html?graph=' + graphStr
}
return url
}
export const imgSnapshotTest = async (page, graphStr, options, api) => {
return new Promise(async resolve => {
const url = mermaidUrl(graphStr, options, api)
await page.goto(url)
const image = await page.screenshot()
expect(image).toMatchImageSnapshot()
resolve()
})
// page.close()
}

View File

@@ -1,11 +0,0 @@
// jest.config.js
module.exports = {
// verbose: true,
transform: {
'^.+\\.jsx?$': '../transformer.js'
},
preset: 'jest-puppeteer',
'globalSetup': 'jest-environment-puppeteer/setup',
'globalTeardown': 'jest-environment-puppeteer/teardown',
'testEnvironment': 'jest-environment-puppeteer'
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -1,15 +0,0 @@
/* eslint-env jest */
import { imgSnapshotTest } from '../helpers/util.js'
const { toMatchImageSnapshot } = require('jest-image-snapshot')
expect.extend({ toMatchImageSnapshot })
describe('Sequencediagram', () => {
it('should render a simple info diagrams', async () => {
await imgSnapshotTest(page, `
info
showInfo
`,
{})
})
})

View File

@@ -1,16 +0,0 @@
/* eslint-env jest */
const { toMatchImageSnapshot } = require('jest-image-snapshot')
expect.extend({ toMatchImageSnapshot })
describe('Sequencediagram', () => {
it('should render a simple sequence diagrams', async () => {
const url = 'http://localhost:9000/webpackUsage.html'
await page.goto(url)
const image = await page.screenshot()
expect(image).toMatchImageSnapshot()
})
})

View File

@@ -1,15 +0,0 @@
/* eslint-env jest */
import { imgSnapshotTest } from '../helpers/util.js'
const { toMatchImageSnapshot } = require('jest-image-snapshot')
expect.extend({ toMatchImageSnapshot })
/* eslint-disable */
describe('XSS', () => {
it('should handle xss in tags', async () => {
// const str = 'graph LR;\nB-->D(<img onerror=location=`javascript\u003aalert\u0028document.domain\u0029` src=x>);'
const str = 'eyJjb2RlIjoiXG5ncmFwaCBMUlxuICAgICAgQi0tPkQoPGltZyBvbmVycm9yPWxvY2F0aW9uPWBqYXZhc2NyaXB0XFx1MDAzYXhzc0F0dGFja1xcdTAwMjhkb2N1bWVudC5kb21haW5cXHUwMDI5YCBzcmM9eD4pOyIsIm1lcm1haWQiOnsidGhlbWUiOiJkZWZhdWx0In19';
await imgSnapshotTest(page, str,
{}, true)
})
})

View File

@@ -1,4 +1,4 @@
const path = require('path')
const path = require('path');
module.exports = {
transform: {
@@ -10,4 +10,4 @@ module.exports = {
'\\.(css|scss)$': 'identity-obj-proxy'
},
moduleFileExtensions: ['js', 'json', 'jsx', 'ts', 'tsx', 'node', 'jison']
}
};

View File

@@ -1,6 +1,6 @@
{
"name": "mermaid",
"version": "8.2.5",
"version": "8.3.0",
"description": "Markdownish syntax for generating flowcharts, sequence diagrams, class diagrams, gantt charts and git graphs.",
"main": "dist/mermaid.core.js",
"keywords": [
@@ -18,10 +18,12 @@
"build:watch": "yarn build --watch",
"minify": "minify ./dist/mermaid.js > ./dist/mermaid.min.js",
"release": "yarn build -p --config webpack.config.prod.babel.js",
"lint": "standard",
"e2e": "yarn lint && jest e2e --config e2e/jest.config.js",
"lint": "eslint src",
"e2e:depr": "yarn lint && jest e2e --config e2e/jest.config.js",
"cypress": "percy exec -- cypress run",
"e2e": "start-server-and-test dev http://localhost:9000/ cypress",
"e2e-upd": "yarn lint && jest e2e -u --config e2e/jest.config.js",
"dev": "yarn lint && webpack-dev-server --config webpack.config.e2e.js",
"dev": "webpack-dev-server --config webpack.config.e2e.js",
"test": "yarn lint && jest src",
"test:watch": "jest --watch src",
"prepublishOnly": "yarn build && yarn release && yarn test",
@@ -36,7 +38,8 @@
"standard": {
"ignore": [
"**/parser/*.js",
"dist/**/*.js"
"dist/**/*.js",
"cypress/**/*.js"
],
"globals": [
"page"
@@ -47,24 +50,30 @@
"d3": "^5.7.0",
"dagre-d3-renderer": "^0.5.8",
"dagre-layout": "^0.8.8",
"documentation": "^12.0.1",
"graphlibrary": "^2.2.0",
"he": "^1.2.0",
"lodash": "^4.17.11",
"minify": "^4.1.1",
"moment-mini": "^2.22.1",
"prettier": "^1.18.2",
"scope-css": "^1.2.1"
},
"devDependencies": {
"documentation": "^12.0.1",
"eslint": "^6.3.0",
"eslint-config-prettier": "^6.3.0",
"eslint-plugin-prettier": "^3.1.0",
"@babel/core": "^7.2.2",
"@babel/preset-env": "^7.2.0",
"@babel/register": "^7.0.0",
"@percy/cypress": "^2.0.1",
"babel-core": "7.0.0-bridge.0",
"babel-jest": "^23.6.0",
"babel-loader": "^8.0.4",
"coveralls": "^3.0.2",
"css-loader": "^2.0.1",
"css-to-string-loader": "^0.1.3",
"cypress": "3.4.0",
"husky": "^1.2.1",
"identity-obj-proxy": "^3.0.0",
"jest": "^23.6.0",
@@ -76,7 +85,7 @@
"node-sass": "^4.11.0",
"puppeteer": "^1.17.0",
"sass-loader": "^7.1.0",
"standard": "^12.0.1",
"start-server-and-test": "^1.10.0",
"webpack": "^4.27.1",
"webpack-cli": "^3.1.2",
"webpack-dev-server": "^3.4.1",

View File

@@ -1,28 +1,27 @@
let config = {
}
let config = {};
const setConf = function (cnf) {
const setConf = function(cnf) {
// Top level initially mermaid, gflow, sequenceDiagram and gantt
const lvl1Keys = Object.keys(cnf)
const lvl1Keys = Object.keys(cnf);
for (let i = 0; i < lvl1Keys.length; i++) {
if (typeof cnf[lvl1Keys[i]] === 'object' && cnf[lvl1Keys[i]] != null) {
const lvl2Keys = Object.keys(cnf[lvl1Keys[i]])
const lvl2Keys = Object.keys(cnf[lvl1Keys[i]]);
for (let j = 0; j < lvl2Keys.length; j++) {
// logger.debug('Setting conf ', lvl1Keys[i], '-', lvl2Keys[j])
if (typeof config[lvl1Keys[i]] === 'undefined') {
config[lvl1Keys[i]] = {}
config[lvl1Keys[i]] = {};
}
// logger.debug('Setting config: ' + lvl1Keys[i] + ' ' + lvl2Keys[j] + ' to ' + cnf[lvl1Keys[i]][lvl2Keys[j]])
config[lvl1Keys[i]][lvl2Keys[j]] = cnf[lvl1Keys[i]][lvl2Keys[j]]
config[lvl1Keys[i]][lvl2Keys[j]] = cnf[lvl1Keys[i]][lvl2Keys[j]];
}
} else {
config[lvl1Keys[i]] = cnf[lvl1Keys[i]]
config[lvl1Keys[i]] = cnf[lvl1Keys[i]];
}
}
}
};
export const setConfig = conf => {
setConf(conf)
}
export const getConfig = () => config
setConf(conf);
};
export const getConfig = () => config;

View File

@@ -1,8 +1,7 @@
import { logger } from '../../logger';
import { logger } from '../../logger'
let relations = []
let classes = {}
let relations = [];
let classes = {};
/**
* Function called by parser when a node definition has been found.
@@ -11,75 +10,75 @@ let classes = {}
* @param type
* @param style
*/
export const addClass = function (id) {
export const addClass = function(id) {
if (typeof classes[id] === 'undefined') {
classes[id] = {
id: id,
methods: [],
members: []
}
};
}
}
};
export const clear = function () {
relations = []
classes = {}
}
export const clear = function() {
relations = [];
classes = {};
};
export const getClass = function (id) {
return classes[id]
}
export const getClasses = function () {
return classes
}
export const getClass = function(id) {
return classes[id];
};
export const getClasses = function() {
return classes;
};
export const getRelations = function () {
return relations
}
export const getRelations = function() {
return relations;
};
export const addRelation = function (relation) {
logger.debug('Adding relation: ' + JSON.stringify(relation))
addClass(relation.id1)
addClass(relation.id2)
relations.push(relation)
}
export const addRelation = function(relation) {
logger.debug('Adding relation: ' + JSON.stringify(relation));
addClass(relation.id1);
addClass(relation.id2);
relations.push(relation);
};
export const addMember = function (className, member) {
const theClass = classes[className]
export const addMember = function(className, member) {
const theClass = classes[className];
if (typeof member === 'string') {
if (member.substr(-1) === ')') {
theClass.methods.push(member)
theClass.methods.push(member);
} else {
theClass.members.push(member)
theClass.members.push(member);
}
}
}
};
export const addMembers = function (className, MembersArr) {
export const addMembers = function(className, MembersArr) {
if (Array.isArray(MembersArr)) {
MembersArr.forEach(member => addMember(className, member))
MembersArr.forEach(member => addMember(className, member));
}
}
};
export const cleanupLabel = function (label) {
export const cleanupLabel = function(label) {
if (label.substring(0, 1) === ':') {
return label.substr(2).trim()
return label.substr(2).trim();
} else {
return label.trim()
return label.trim();
}
}
};
export const lineType = {
LINE: 0,
DOTTED_LINE: 1
}
};
export const relationType = {
AGGREGATION: 0,
EXTENSION: 1,
COMPOSITION: 2,
DEPENDENCY: 3
}
};
export default {
addClass,
@@ -93,4 +92,4 @@ export default {
cleanupLabel,
lineType,
relationType
}
};

View File

@@ -1,208 +1,211 @@
/* eslint-env jasmine */
import { parser } from './parser/classDiagram'
import classDb from './classDb'
import { parser } from './parser/classDiagram';
import classDb from './classDb';
describe('class diagram, ', function () {
describe('when parsing an info graph it', function () {
beforeEach(function () {
parser.yy = classDb
})
describe('class diagram, ', function() {
describe('when parsing an info graph it', function() {
beforeEach(function() {
parser.yy = classDb;
});
it('should handle relation definitions', function () {
const str = 'classDiagram\n' +
'Class01 <|-- Class02\n' +
'Class03 *-- Class04\n' +
'Class05 o-- Class06\n' +
'Class07 .. Class08\n' +
'Class09 -- Class1'
it('should handle relation definitions', function() {
const str =
'classDiagram\n' +
'Class01 <|-- Class02\n' +
'Class03 *-- Class04\n' +
'Class05 o-- Class06\n' +
'Class07 .. Class08\n' +
'Class09 -- Class1';
parser.parse(str)
})
it('should handle relation definition of different types and directions', function () {
const str = 'classDiagram\n' +
'Class11 <|.. Class12\n' +
'Class13 --> Class14\n' +
'Class15 ..> Class16\n' +
'Class17 ..|> Class18\n' +
'Class19 <--* Class20'
parser.parse(str);
});
it('should handle relation definition of different types and directions', function() {
const str =
'classDiagram\n' +
'Class11 <|.. Class12\n' +
'Class13 --> Class14\n' +
'Class15 ..> Class16\n' +
'Class17 ..|> Class18\n' +
'Class19 <--* Class20';
parser.parse(str)
})
parser.parse(str);
});
it('should handle cardinality and labels', function () {
const str = 'classDiagram\n' +
'Class01 "1" *-- "many" Class02 : contains\n' +
'Class03 o-- Class04 : aggregation\n' +
'Class05 --> "1" Class06'
it('should handle cardinality and labels', function() {
const str =
'classDiagram\n' +
'Class01 "1" *-- "many" Class02 : contains\n' +
'Class03 o-- Class04 : aggregation\n' +
'Class05 --> "1" Class06';
parser.parse(str)
})
it('should handle class definitions', function () {
const str = 'classDiagram\n' +
'class Car\n' +
'Driver -- Car : drives >\n' +
'Car *-- Wheel : have 4 >\n' +
'Car -- Person : < owns'
parser.parse(str);
});
it('should handle class definitions', function() {
const str =
'classDiagram\n' +
'class Car\n' +
'Driver -- Car : drives >\n' +
'Car *-- Wheel : have 4 >\n' +
'Car -- Person : < owns';
parser.parse(str)
})
parser.parse(str);
});
it('should handle method statements', function () {
const str = 'classDiagram\n' +
'Object <|-- ArrayList\n' +
'Object : equals()\n' +
'ArrayList : Object[] elementData\n' +
'ArrayList : size()'
it('should handle method statements', function() {
const str =
'classDiagram\n' +
'Object <|-- ArrayList\n' +
'Object : equals()\n' +
'ArrayList : Object[] elementData\n' +
'ArrayList : size()';
parser.parse(str)
})
it('should handle parsing of method statements grouped by brackets', function () {
const str = 'classDiagram\n' +
'class Dummy {\n' +
'String data\n' +
' void methods()\n' +
'}\n' +
'\n' +
'class Flight {\n' +
' flightNumber : Integer\n' +
' departureTime : Date\n' +
'}'
parser.parse(str);
});
it('should handle parsing of method statements grouped by brackets', function() {
const str =
'classDiagram\n' +
'class Dummy {\n' +
'String data\n' +
' void methods()\n' +
'}\n' +
'\n' +
'class Flight {\n' +
' flightNumber : Integer\n' +
' departureTime : Date\n' +
'}';
parser.parse(str)
})
parser.parse(str);
});
it('should handle parsing of separators', function () {
const str = 'classDiagram\n' +
'class Foo1 {\n' +
' You can use\n' +
' several lines\n' +
'..\n' +
'as you want\n' +
'and group\n' +
'==\n' +
'things together.\n' +
'__\n' +
'You can have as many groups\n' +
'as you want\n' +
'--\n' +
'End of class\n' +
'}\n' +
'\n' +
'class User {\n' +
'.. Simple Getter ..\n' +
'+ getName()\n' +
'+ getAddress()\n' +
'.. Some setter ..\n' +
'+ setName()\n' +
'__ private data __\n' +
'int age\n' +
'-- encrypted --\n' +
'String password\n' +
'}'
it('should handle parsing of separators', function() {
const str =
'classDiagram\n' +
'class Foo1 {\n' +
' You can use\n' +
' several lines\n' +
'..\n' +
'as you want\n' +
'and group\n' +
'==\n' +
'things together.\n' +
'__\n' +
'You can have as many groups\n' +
'as you want\n' +
'--\n' +
'End of class\n' +
'}\n' +
'\n' +
'class User {\n' +
'.. Simple Getter ..\n' +
'+ getName()\n' +
'+ getAddress()\n' +
'.. Some setter ..\n' +
'+ setName()\n' +
'__ private data __\n' +
'int age\n' +
'-- encrypted --\n' +
'String password\n' +
'}';
parser.parse(str)
})
})
parser.parse(str);
});
});
describe('when fetching data from an classDiagram graph it', function () {
beforeEach(function () {
parser.yy = classDb
parser.yy.clear()
})
it('should handle relation definitions EXTENSION', function () {
const str = 'classDiagram\n' +
'Class01 <|-- Class02'
describe('when fetching data from an classDiagram graph it', function() {
beforeEach(function() {
parser.yy = classDb;
parser.yy.clear();
});
it('should handle relation definitions EXTENSION', function() {
const str = 'classDiagram\n' + 'Class01 <|-- Class02';
parser.parse(str)
parser.parse(str);
const relations = parser.yy.getRelations()
const relations = parser.yy.getRelations();
expect(parser.yy.getClass('Class01').id).toBe('Class01')
expect(parser.yy.getClass('Class02').id).toBe('Class02')
expect(relations[0].relation.type1).toBe(classDb.relationType.EXTENSION)
expect(relations[0].relation.type2).toBe('none')
expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE)
})
it('should handle relation definitions AGGREGATION and dotted line', function () {
const str = 'classDiagram\n' +
'Class01 o.. Class02'
expect(parser.yy.getClass('Class01').id).toBe('Class01');
expect(parser.yy.getClass('Class02').id).toBe('Class02');
expect(relations[0].relation.type1).toBe(classDb.relationType.EXTENSION);
expect(relations[0].relation.type2).toBe('none');
expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE);
});
it('should handle relation definitions AGGREGATION and dotted line', function() {
const str = 'classDiagram\n' + 'Class01 o.. Class02';
parser.parse(str)
parser.parse(str);
const relations = parser.yy.getRelations()
const relations = parser.yy.getRelations();
expect(parser.yy.getClass('Class01').id).toBe('Class01')
expect(parser.yy.getClass('Class02').id).toBe('Class02')
expect(relations[0].relation.type1).toBe(classDb.relationType.AGGREGATION)
expect(relations[0].relation.type2).toBe('none')
expect(relations[0].relation.lineType).toBe(classDb.lineType.DOTTED_LINE)
})
it('should handle relation definitions COMPOSITION on both sides', function () {
const str = 'classDiagram\n' +
'Class01 *--* Class02'
expect(parser.yy.getClass('Class01').id).toBe('Class01');
expect(parser.yy.getClass('Class02').id).toBe('Class02');
expect(relations[0].relation.type1).toBe(classDb.relationType.AGGREGATION);
expect(relations[0].relation.type2).toBe('none');
expect(relations[0].relation.lineType).toBe(classDb.lineType.DOTTED_LINE);
});
it('should handle relation definitions COMPOSITION on both sides', function() {
const str = 'classDiagram\n' + 'Class01 *--* Class02';
parser.parse(str)
parser.parse(str);
const relations = parser.yy.getRelations()
const relations = parser.yy.getRelations();
expect(parser.yy.getClass('Class01').id).toBe('Class01')
expect(parser.yy.getClass('Class02').id).toBe('Class02')
expect(relations[0].relation.type1).toBe(classDb.relationType.COMPOSITION)
expect(relations[0].relation.type2).toBe(classDb.relationType.COMPOSITION)
expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE)
})
it('should handle relation definitions no types', function () {
const str = 'classDiagram\n' +
'Class01 -- Class02'
expect(parser.yy.getClass('Class01').id).toBe('Class01');
expect(parser.yy.getClass('Class02').id).toBe('Class02');
expect(relations[0].relation.type1).toBe(classDb.relationType.COMPOSITION);
expect(relations[0].relation.type2).toBe(classDb.relationType.COMPOSITION);
expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE);
});
it('should handle relation definitions no types', function() {
const str = 'classDiagram\n' + 'Class01 -- Class02';
parser.parse(str)
parser.parse(str);
const relations = parser.yy.getRelations()
const relations = parser.yy.getRelations();
expect(parser.yy.getClass('Class01').id).toBe('Class01')
expect(parser.yy.getClass('Class02').id).toBe('Class02')
expect(relations[0].relation.type1).toBe('none')
expect(relations[0].relation.type2).toBe('none')
expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE)
})
it('should handle relation definitions with type only on right side', function () {
const str = 'classDiagram\n' +
'Class01 --|> Class02'
expect(parser.yy.getClass('Class01').id).toBe('Class01');
expect(parser.yy.getClass('Class02').id).toBe('Class02');
expect(relations[0].relation.type1).toBe('none');
expect(relations[0].relation.type2).toBe('none');
expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE);
});
it('should handle relation definitions with type only on right side', function() {
const str = 'classDiagram\n' + 'Class01 --|> Class02';
parser.parse(str)
parser.parse(str);
const relations = parser.yy.getRelations()
const relations = parser.yy.getRelations();
expect(parser.yy.getClass('Class01').id).toBe('Class01')
expect(parser.yy.getClass('Class02').id).toBe('Class02')
expect(relations[0].relation.type1).toBe('none')
expect(relations[0].relation.type2).toBe(classDb.relationType.EXTENSION)
expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE)
})
expect(parser.yy.getClass('Class01').id).toBe('Class01');
expect(parser.yy.getClass('Class02').id).toBe('Class02');
expect(relations[0].relation.type1).toBe('none');
expect(relations[0].relation.type2).toBe(classDb.relationType.EXTENSION);
expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE);
});
it('should handle multiple classes and relation definitions', function () {
const str = 'classDiagram\n' +
'Class01 <|-- Class02\n' +
'Class03 *-- Class04\n' +
'Class05 o-- Class06\n' +
'Class07 .. Class08\n' +
'Class09 -- Class10'
it('should handle multiple classes and relation definitions', function() {
const str =
'classDiagram\n' +
'Class01 <|-- Class02\n' +
'Class03 *-- Class04\n' +
'Class05 o-- Class06\n' +
'Class07 .. Class08\n' +
'Class09 -- Class10';
parser.parse(str)
parser.parse(str);
const relations = parser.yy.getRelations()
const relations = parser.yy.getRelations();
expect(parser.yy.getClass('Class01').id).toBe('Class01')
expect(parser.yy.getClass('Class10').id).toBe('Class10')
expect(parser.yy.getClass('Class01').id).toBe('Class01');
expect(parser.yy.getClass('Class10').id).toBe('Class10');
expect(relations.length).toBe(5)
expect(relations.length).toBe(5);
expect(relations[0].relation.type1).toBe(classDb.relationType.EXTENSION)
expect(relations[0].relation.type2).toBe('none')
expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE)
expect(relations[3].relation.type1).toBe('none')
expect(relations[3].relation.type2).toBe('none')
expect(relations[3].relation.lineType).toBe(classDb.lineType.DOTTED_LINE)
})
})
})
expect(relations[0].relation.type1).toBe(classDb.relationType.EXTENSION);
expect(relations[0].relation.type2).toBe('none');
expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE);
expect(relations[3].relation.type1).toBe('none');
expect(relations[3].relation.type2).toBe('none');
expect(relations[3].relation.lineType).toBe(classDb.lineType.DOTTED_LINE);
});
});
});

View File

@@ -1,38 +1,38 @@
import * as d3 from 'd3'
import dagre from 'dagre-layout'
import graphlib from 'graphlibrary'
import { logger } from '../../logger'
import classDb from './classDb'
import { parser } from './parser/classDiagram'
import * as d3 from 'd3';
import dagre from 'dagre-layout';
import graphlib from 'graphlibrary';
import { logger } from '../../logger';
import classDb from './classDb';
import { parser } from './parser/classDiagram';
parser.yy = classDb
parser.yy = classDb;
const idCache = {}
const idCache = {};
let classCnt = 0
let classCnt = 0;
const conf = {
dividerMargin: 10,
padding: 5,
textHeight: 10
}
};
// Todo optimize
const getGraphId = function (label) {
const keys = Object.keys(idCache)
const getGraphId = function(label) {
const keys = Object.keys(idCache);
for (let i = 0; i < keys.length; i++) {
if (idCache[keys[i]].label === label) {
return keys[i]
return keys[i];
}
}
return undefined
}
return undefined;
};
/**
* Setup arrow head and define the marker. The result is appended to the svg.
*/
const insertMarkers = function (elem) {
const insertMarkers = function(elem) {
elem
.append('defs')
.append('marker')
@@ -44,7 +44,7 @@ const insertMarkers = function (elem) {
.attr('markerHeight', 240)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M 1,7 L18,13 V 1 Z')
.attr('d', 'M 1,7 L18,13 V 1 Z');
elem
.append('defs')
@@ -56,7 +56,7 @@ const insertMarkers = function (elem) {
.attr('markerHeight', 28)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M 1,1 V 13 L18,7 Z') // this is actual shape for arrowhead
.attr('d', 'M 1,1 V 13 L18,7 Z'); // this is actual shape for arrowhead
elem
.append('defs')
@@ -69,7 +69,7 @@ const insertMarkers = function (elem) {
.attr('markerHeight', 240)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z')
.attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z');
elem
.append('defs')
@@ -81,7 +81,7 @@ const insertMarkers = function (elem) {
.attr('markerHeight', 28)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z')
.attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z');
elem
.append('defs')
@@ -94,7 +94,7 @@ const insertMarkers = function (elem) {
.attr('markerHeight', 240)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z')
.attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z');
elem
.append('defs')
@@ -106,7 +106,7 @@ const insertMarkers = function (elem) {
.attr('markerHeight', 28)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z')
.attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z');
elem
.append('defs')
@@ -119,7 +119,7 @@ const insertMarkers = function (elem) {
.attr('markerHeight', 240)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M 5,7 L9,13 L1,7 L9,1 Z')
.attr('d', 'M 5,7 L9,13 L1,7 L9,1 Z');
elem
.append('defs')
@@ -131,96 +131,86 @@ const insertMarkers = function (elem) {
.attr('markerHeight', 28)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M 18,7 L9,13 L14,7 L9,1 Z')
}
.attr('d', 'M 18,7 L9,13 L14,7 L9,1 Z');
};
let edgeCount = 0
let total = 0
const drawEdge = function (elem, path, relation) {
const getRelationType = function (type) {
let edgeCount = 0;
let total = 0;
const drawEdge = function(elem, path, relation) {
const getRelationType = function(type) {
switch (type) {
case classDb.relationType.AGGREGATION:
return 'aggregation'
return 'aggregation';
case classDb.relationType.EXTENSION:
return 'extension'
return 'extension';
case classDb.relationType.COMPOSITION:
return 'composition'
return 'composition';
case classDb.relationType.DEPENDENCY:
return 'dependency'
return 'dependency';
}
}
};
path.points = path.points.filter(p => !Number.isNaN(p.y))
path.points = path.points.filter(p => !Number.isNaN(p.y));
// The data for our line
const lineData = path.points
const lineData = path.points;
// This is the accessor function we talked about above
const lineFunction = d3
.line()
.x(function (d) {
return d.x
.x(function(d) {
return d.x;
})
.y(function (d) {
return d.y
.y(function(d) {
return d.y;
})
.curve(d3.curveBasis)
.curve(d3.curveBasis);
const svgPath = elem
.append('path')
.attr('d', lineFunction(lineData))
.attr('id', 'edge' + edgeCount)
.attr('class', 'relation')
let url = ''
.attr('class', 'relation');
let url = '';
if (conf.arrowMarkerAbsolute) {
url =
window.location.protocol +
'//' +
window.location.host +
window.location.pathname +
window.location.search
url = url.replace(/\(/g, '\\(')
url = url.replace(/\)/g, '\\)')
window.location.search;
url = url.replace(/\(/g, '\\(');
url = url.replace(/\)/g, '\\)');
}
if (relation.relation.type1 !== 'none') {
svgPath.attr(
'marker-start',
'url(' +
url +
'#' +
getRelationType(relation.relation.type1) +
'Start' +
')'
)
'url(' + url + '#' + getRelationType(relation.relation.type1) + 'Start' + ')'
);
}
if (relation.relation.type2 !== 'none') {
svgPath.attr(
'marker-end',
'url(' +
url +
'#' +
getRelationType(relation.relation.type2) +
'End' +
')'
)
'url(' + url + '#' + getRelationType(relation.relation.type2) + 'End' + ')'
);
}
let x, y
const l = path.points.length
let x, y;
const l = path.points.length;
if (l % 2 !== 0 && l > 1) {
const p1 = path.points[Math.floor(l / 2)]
const p2 = path.points[Math.ceil(l / 2)]
x = (p1.x + p2.x) / 2
y = (p1.y + p2.y) / 2
const p1 = path.points[Math.floor(l / 2)];
const p2 = path.points[Math.ceil(l / 2)];
x = (p1.x + p2.x) / 2;
y = (p1.y + p2.y) / 2;
} else {
const p = path.points[Math.floor(l / 2)]
x = p.x
y = p.y
const p = path.points[Math.floor(l / 2)];
x = p.x;
y = p.y;
}
if (typeof relation.title !== 'undefined') {
const g = elem.append('g').attr('class', 'classLabel')
const g = elem.append('g').attr('class', 'classLabel');
const label = g
.append('text')
.attr('class', 'label')
@@ -228,189 +218,177 @@ const drawEdge = function (elem, path, relation) {
.attr('y', y)
.attr('fill', 'red')
.attr('text-anchor', 'middle')
.text(relation.title)
.text(relation.title);
window.label = label
const bounds = label.node().getBBox()
window.label = label;
const bounds = label.node().getBBox();
g.insert('rect', ':first-child')
.attr('class', 'box')
.attr('x', bounds.x - conf.padding / 2)
.attr('y', bounds.y - conf.padding / 2)
.attr('width', bounds.width + conf.padding)
.attr('height', bounds.height + conf.padding)
.attr('height', bounds.height + conf.padding);
}
edgeCount++
}
edgeCount++;
};
const drawClass = function (elem, classDef) {
logger.info('Rendering class ' + classDef)
const drawClass = function(elem, classDef) {
logger.info('Rendering class ' + classDef);
const addTspan = function (textEl, txt, isFirst) {
const addTspan = function(textEl, txt, isFirst) {
const tSpan = textEl
.append('tspan')
.attr('x', conf.padding)
.text(txt)
.text(txt);
if (!isFirst) {
tSpan.attr('dy', conf.textHeight)
tSpan.attr('dy', conf.textHeight);
}
}
};
const id = 'classId' + (classCnt % total)
const id = 'classId' + (classCnt % total);
const classInfo = {
id: id,
label: classDef.id,
width: 0,
height: 0
}
};
const g = elem
.append('g')
.attr('id', id)
.attr('class', 'classGroup')
.attr('class', 'classGroup');
const title = g
.append('text')
.attr('x', conf.padding)
.attr('y', conf.textHeight + conf.padding)
.text(classDef.id)
.text(classDef.id);
const titleHeight = title.node().getBBox().height
const titleHeight = title.node().getBBox().height;
const membersLine = g
.append('line') // text label for the x axis
.attr('x1', 0)
.attr('y1', conf.padding + titleHeight + conf.dividerMargin / 2)
.attr('y2', conf.padding + titleHeight + conf.dividerMargin / 2)
.attr('y2', conf.padding + titleHeight + conf.dividerMargin / 2);
const members = g
.append('text') // text label for the x axis
.attr('x', conf.padding)
.attr('y', titleHeight + conf.dividerMargin + conf.textHeight)
.attr('fill', 'white')
.attr('class', 'classText')
.attr('class', 'classText');
let isFirst = true
classDef.members.forEach(function (member) {
addTspan(members, member, isFirst)
isFirst = false
})
let isFirst = true;
classDef.members.forEach(function(member) {
addTspan(members, member, isFirst);
isFirst = false;
});
const membersBox = members.node().getBBox()
const membersBox = members.node().getBBox();
const methodsLine = g
.append('line') // text label for the x axis
.attr('x1', 0)
.attr(
'y1',
conf.padding + titleHeight + conf.dividerMargin + membersBox.height
)
.attr(
'y2',
conf.padding + titleHeight + conf.dividerMargin + membersBox.height
)
.attr('y1', conf.padding + titleHeight + conf.dividerMargin + membersBox.height)
.attr('y2', conf.padding + titleHeight + conf.dividerMargin + membersBox.height);
const methods = g
.append('text') // text label for the x axis
.attr('x', conf.padding)
.attr(
'y',
titleHeight + 2 * conf.dividerMargin + membersBox.height + conf.textHeight
)
.attr('y', titleHeight + 2 * conf.dividerMargin + membersBox.height + conf.textHeight)
.attr('fill', 'white')
.attr('class', 'classText')
.attr('class', 'classText');
isFirst = true
isFirst = true;
classDef.methods.forEach(function (method) {
addTspan(methods, method, isFirst)
isFirst = false
})
classDef.methods.forEach(function(method) {
addTspan(methods, method, isFirst);
isFirst = false;
});
const classBox = g.node().getBBox()
const classBox = g.node().getBBox();
g.insert('rect', ':first-child')
.attr('x', 0)
.attr('y', 0)
.attr('width', classBox.width + 2 * conf.padding)
.attr('height', classBox.height + conf.padding + 0.5 * conf.dividerMargin)
.attr('height', classBox.height + conf.padding + 0.5 * conf.dividerMargin);
membersLine.attr('x2', classBox.width + 2 * conf.padding)
methodsLine.attr('x2', classBox.width + 2 * conf.padding)
membersLine.attr('x2', classBox.width + 2 * conf.padding);
methodsLine.attr('x2', classBox.width + 2 * conf.padding);
classInfo.width = classBox.width + 2 * conf.padding
classInfo.height = classBox.height + conf.padding + 0.5 * conf.dividerMargin
classInfo.width = classBox.width + 2 * conf.padding;
classInfo.height = classBox.height + conf.padding + 0.5 * conf.dividerMargin;
idCache[id] = classInfo
classCnt++
return classInfo
}
idCache[id] = classInfo;
classCnt++;
return classInfo;
};
export const setConf = function (cnf) {
const keys = Object.keys(cnf)
export const setConf = function(cnf) {
const keys = Object.keys(cnf);
keys.forEach(function (key) {
conf[key] = cnf[key]
})
}
keys.forEach(function(key) {
conf[key] = cnf[key];
});
};
/**
* Draws a flowchart in the tag with id: id based on the graph definition in text.
* @param text
* @param id
*/
export const draw = function (text, id) {
parser.yy.clear()
parser.parse(text)
export const draw = function(text, id) {
parser.yy.clear();
parser.parse(text);
logger.info('Rendering diagram ' + text)
logger.info('Rendering diagram ' + text);
/// / Fetch the default direction, use TD if none was found
const diagram = d3.select(`[id='${id}']`)
insertMarkers(diagram)
const diagram = d3.select(`[id='${id}']`);
insertMarkers(diagram);
// Layout graph, Create a new directed graph
const g = new graphlib.Graph({
multigraph: true
})
});
// Set an object for the graph label
g.setGraph({
isMultiGraph: true
})
});
// Default to assigning a new object as a label for each new edge.
g.setDefaultEdgeLabel(function () {
return {}
})
g.setDefaultEdgeLabel(function() {
return {};
});
const classes = classDb.getClasses()
const keys = Object.keys(classes)
total = keys.length
const classes = classDb.getClasses();
const keys = Object.keys(classes);
total = keys.length;
for (let i = 0; i < keys.length; i++) {
const classDef = classes[keys[i]]
const node = drawClass(diagram, classDef)
const classDef = classes[keys[i]];
const node = drawClass(diagram, classDef);
// Add nodes to the graph. The first argument is the node id. The second is
// metadata about the node. In this case we're going to add labels to each of
// our nodes.
g.setNode(node.id, node)
logger.info('Org height: ' + node.height)
g.setNode(node.id, node);
logger.info('Org height: ' + node.height);
}
const relations = classDb.getRelations()
relations.forEach(function (relation) {
const relations = classDb.getRelations();
relations.forEach(function(relation) {
logger.info(
'tjoho' +
getGraphId(relation.id1) +
getGraphId(relation.id2) +
JSON.stringify(relation)
)
'tjoho' + getGraphId(relation.id1) + getGraphId(relation.id2) + JSON.stringify(relation)
);
g.setEdge(getGraphId(relation.id1), getGraphId(relation.id2), {
relation: relation
})
})
dagre.layout(g)
g.nodes().forEach(function (v) {
});
});
dagre.layout(g);
g.nodes().forEach(function(v) {
if (typeof v !== 'undefined' && typeof g.node(v) !== 'undefined') {
logger.debug('Node ' + v + ': ' + JSON.stringify(g.node(v)))
logger.debug('Node ' + v + ': ' + JSON.stringify(g.node(v)));
d3.select('#' + v).attr(
'transform',
'translate(' +
@@ -418,27 +396,22 @@ export const draw = function (text, id) {
',' +
(g.node(v).y - g.node(v).height / 2) +
' )'
)
);
}
})
g.edges().forEach(function (e) {
});
g.edges().forEach(function(e) {
if (typeof e !== 'undefined' && typeof g.edge(e) !== 'undefined') {
logger.debug(
'Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(g.edge(e))
)
drawEdge(diagram, g.edge(e), g.edge(e).relation)
logger.debug('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(g.edge(e)));
drawEdge(diagram, g.edge(e), g.edge(e).relation);
}
})
});
diagram.attr('height', '100%')
diagram.attr('width', '100%')
diagram.attr(
'viewBox',
'0 0 ' + (g.graph().width + 20) + ' ' + (g.graph().height + 20)
)
}
diagram.attr('height', '100%');
diagram.attr('width', '100%');
diagram.attr('viewBox', '0 0 ' + (g.graph().width + 20) + ' ' + (g.graph().height + 20));
};
export default {
setConf,
draw
}
};

View File

@@ -1,33 +1,34 @@
import * as d3 from 'd3'
import { sanitizeUrl } from '@braintree/sanitize-url'
import { logger } from '../../logger'
import utils from '../../utils'
import { getConfig } from '../../config'
import * as d3 from 'd3';
import { sanitizeUrl } from '@braintree/sanitize-url';
import { logger } from '../../logger';
import utils from '../../utils';
import { getConfig } from '../../config';
const config = getConfig()
let vertices = {}
let edges = []
let classes = []
let subGraphs = []
let subGraphLookup = {}
let tooltips = {}
let subCount = 0
let direction
const config = getConfig();
let vertices = {};
let edges = [];
let classes = [];
let subGraphs = [];
let subGraphLookup = {};
let tooltips = {};
let subCount = 0;
let firstGraphFlag = true;
let direction;
// Functions to be run after graph rendering
let funs = []
let funs = [];
const sanitize = text => {
let txt = text
let txt = text;
if (config.securityLevel !== 'loose') {
txt = txt.replace(/<br>/g, '#br#')
txt = txt.replace(/<br\S*?\/>/g, '#br#')
txt = txt.replace(/</g, '&lt;').replace(/>/g, '&gt;')
txt = txt.replace(/=/g, '&equals;')
txt = txt.replace(/#br#/g, '<br/>')
txt = txt.replace(/<br>/g, '#br#');
txt = txt.replace(/<br\S*?\/>/g, '#br#');
txt = txt.replace(/</g, '&lt;').replace(/>/g, '&gt;');
txt = txt.replace(/=/g, '&equals;');
txt = txt.replace(/#br#/g, '<br/>');
}
return txt
}
return txt;
};
/**
* Function called by parser when a node definition has been found
@@ -37,49 +38,53 @@ const sanitize = text => {
* @param style
* @param classes
*/
export const addVertex = function (_id, text, type, style, classes) {
let txt
let id = _id
export const addVertex = function(_id, text, type, style, classes) {
let txt;
let id = _id;
if (typeof id === 'undefined') {
return
return;
}
if (id.trim().length === 0) {
return
return;
}
if (id[0].match(/\d/)) id = 's' + id
if (id[0].match(/\d/)) id = 's' + id;
if (typeof vertices[id] === 'undefined') {
vertices[id] = { id: id, styles: [], classes: [] }
vertices[id] = { id: id, styles: [], classes: [] };
}
if (typeof text !== 'undefined') {
txt = sanitize(text.trim())
txt = sanitize(text.trim());
// strip quotes if string starts and exnds with a quote
if (txt[0] === '"' && txt[txt.length - 1] === '"') {
txt = txt.substring(1, txt.length - 1)
txt = txt.substring(1, txt.length - 1);
}
vertices[id].text = txt
vertices[id].text = txt;
} else {
if (!vertices[id].text) {
vertices[id].text = _id;
}
}
if (typeof type !== 'undefined') {
vertices[id].type = type
vertices[id].type = type;
}
if (typeof style !== 'undefined') {
if (style !== null) {
style.forEach(function (s) {
vertices[id].styles.push(s)
})
style.forEach(function(s) {
vertices[id].styles.push(s);
});
}
}
if (typeof classes !== 'undefined') {
if (classes !== null) {
classes.forEach(function (s) {
vertices[id].classes.push(s)
})
classes.forEach(function(s) {
vertices[id].classes.push(s);
});
}
}
}
};
/**
* Function called by parser when a link/edge definition has been found
@@ -88,130 +93,150 @@ export const addVertex = function (_id, text, type, style, classes) {
* @param type
* @param linktext
*/
export const addLink = function (_start, _end, type, linktext) {
let start = _start
let end = _end
if (start[0].match(/\d/)) start = 's' + start
if (end[0].match(/\d/)) end = 's' + end
logger.info('Got edge...', start, end)
export const addLink = function(_start, _end, type, linktext) {
let start = _start;
let end = _end;
if (start[0].match(/\d/)) start = 's' + start;
if (end[0].match(/\d/)) end = 's' + end;
logger.info('Got edge...', start, end);
const edge = { start: start, end: end, type: undefined, text: '' }
linktext = type.text
const edge = { start: start, end: end, type: undefined, text: '' };
linktext = type.text;
if (typeof linktext !== 'undefined') {
edge.text = sanitize(linktext.trim())
edge.text = sanitize(linktext.trim());
// strip quotes if string starts and exnds with a quote
if (edge.text[0] === '"' && edge.text[edge.text.length - 1] === '"') {
edge.text = edge.text.substring(1, edge.text.length - 1)
edge.text = edge.text.substring(1, edge.text.length - 1);
}
}
if (typeof type !== 'undefined') {
edge.type = type.type
edge.stroke = type.stroke
edge.type = type.type;
edge.stroke = type.stroke;
}
edges.push(edge)
}
edges.push(edge);
};
/**
* Updates a link's line interpolation algorithm
* @param pos
* @param interpolate
*/
export const updateLinkInterpolate = function (positions, interp) {
positions.forEach(function (pos) {
export const updateLinkInterpolate = function(positions, interp) {
positions.forEach(function(pos) {
if (pos === 'default') {
edges.defaultInterpolate = interp
edges.defaultInterpolate = interp;
} else {
edges[pos].interpolate = interp
edges[pos].interpolate = interp;
}
})
}
});
};
/**
* Updates a link with a style
* @param pos
* @param style
*/
export const updateLink = function (positions, style) {
positions.forEach(function (pos) {
export const updateLink = function(positions, style) {
positions.forEach(function(pos) {
if (pos === 'default') {
edges.defaultStyle = style
edges.defaultStyle = style;
} else {
if (utils.isSubstringInArray('fill', style) === -1) {
style.push('fill:none')
style.push('fill:none');
}
edges[pos].style = style
edges[pos].style = style;
}
})
}
});
};
export const addClass = function (id, style) {
export const addClass = function(id, style) {
if (typeof classes[id] === 'undefined') {
classes[id] = { id: id, styles: [] }
classes[id] = { id: id, styles: [] };
}
if (typeof style !== 'undefined') {
if (style !== null) {
style.forEach(function (s) {
classes[id].styles.push(s)
})
style.forEach(function(s) {
classes[id].styles.push(s);
});
}
}
}
};
/**
* Called by parser when a graph definition is found, stores the direction of the chart.
* @param dir
*/
export const setDirection = function (dir) {
direction = dir
}
export const setDirection = function(dir) {
direction = dir;
if (direction.match(/.*</)) {
direction = 'RL';
}
if (direction.match(/.*\^/)) {
direction = 'BT';
}
if (direction.match(/.*>/)) {
direction = 'LR';
}
if (direction.match(/.*v/)) {
direction = 'TB';
}
};
/**
* Called by parser when a special node is found, e.g. a clickable element.
* @param ids Comma separated list of ids
* @param className Class to add
*/
export const setClass = function (ids, className) {
ids.split(',').forEach(function (id) {
export const setClass = function(ids, className) {
ids.split(',').forEach(function(_id) {
let id = _id;
if (_id[0].match(/\d/)) id = 's' + id;
if (typeof vertices[id] !== 'undefined') {
vertices[id].classes.push(className)
vertices[id].classes.push(className);
}
if (typeof subGraphLookup[id] !== 'undefined') {
subGraphLookup[id].classes.push(className)
subGraphLookup[id].classes.push(className);
}
})
}
});
};
const setTooltip = function (ids, tooltip) {
ids.split(',').forEach(function (id) {
const setTooltip = function(ids, tooltip) {
ids.split(',').forEach(function(id) {
if (typeof tooltip !== 'undefined') {
tooltips[id] = sanitize(tooltip)
tooltips[id] = sanitize(tooltip);
}
})
}
});
};
const setClickFun = function (id, functionName) {
const setClickFun = function(_id, functionName) {
let id = _id;
if (_id[0].match(/\d/)) id = 's' + id;
if (config.securityLevel !== 'loose') {
return
return;
}
if (typeof functionName === 'undefined') {
return
return;
}
if (typeof vertices[id] !== 'undefined') {
funs.push(function (element) {
const elem = document.querySelector(`[id="${id}"]`)
funs.push(function(element) {
const elem = document.querySelector(`[id="${id}"]`);
if (elem !== null) {
elem.addEventListener('click', function () {
window[functionName](id)
}, false)
elem.addEventListener(
'click',
function() {
window[functionName](id);
},
false
);
}
})
});
}
}
};
/**
* Called by parser when a link is found. Adds the URL to the vertex data.
@@ -219,22 +244,24 @@ const setClickFun = function (id, functionName) {
* @param linkStr URL to create a link for
* @param tooltip Tooltip for the clickable element
*/
export const setLink = function (ids, linkStr, tooltip) {
ids.split(',').forEach(function (id) {
export const setLink = function(ids, linkStr, tooltip) {
ids.split(',').forEach(function(_id) {
let id = _id;
if (_id[0].match(/\d/)) id = 's' + id;
if (typeof vertices[id] !== 'undefined') {
if (config.securityLevel !== 'loose') {
vertices[id].link = sanitizeUrl(linkStr) // .replace(/javascript:.*/g, '')
vertices[id].link = sanitizeUrl(linkStr); // .replace(/javascript:.*/g, '')
} else {
vertices[id].link = linkStr
vertices[id].link = linkStr;
}
}
})
setTooltip(ids, tooltip)
setClass(ids, 'clickable')
}
export const getTooltip = function (id) {
return tooltips[id]
}
});
setTooltip(ids, tooltip);
setClass(ids, 'clickable');
};
export const getTooltip = function(id) {
return tooltips[id];
};
/**
* Called by parser when a click definition is found. Registers an event handler.
@@ -242,206 +269,228 @@ export const getTooltip = function (id) {
* @param functionName Function to be called on click
* @param tooltip Tooltip for the clickable element
*/
export const setClickEvent = function (ids, functionName, tooltip) {
ids.split(',').forEach(function (id) { setClickFun(id, functionName) })
setTooltip(ids, tooltip)
setClass(ids, 'clickable')
}
export const setClickEvent = function(ids, functionName, tooltip) {
ids.split(',').forEach(function(id) {
setClickFun(id, functionName);
});
setTooltip(ids, tooltip);
setClass(ids, 'clickable');
};
export const bindFunctions = function (element) {
funs.forEach(function (fun) {
fun(element)
})
}
export const getDirection = function () {
return direction
}
export const bindFunctions = function(element) {
funs.forEach(function(fun) {
fun(element);
});
};
export const getDirection = function() {
return direction;
};
/**
* Retrieval function for fetching the found nodes after parsing has completed.
* @returns {{}|*|vertices}
*/
export const getVertices = function () {
return vertices
}
export const getVertices = function() {
return vertices;
};
/**
* Retrieval function for fetching the found links after parsing has completed.
* @returns {{}|*|edges}
*/
export const getEdges = function () {
return edges
}
export const getEdges = function() {
return edges;
};
/**
* Retrieval function for fetching the found class definitions after parsing has completed.
* @returns {{}|*|classes}
*/
export const getClasses = function () {
return classes
}
export const getClasses = function() {
return classes;
};
const setupToolTips = function (element) {
let tooltipElem = d3.select('.mermaidTooltip')
const setupToolTips = function(element) {
let tooltipElem = d3.select('.mermaidTooltip');
if ((tooltipElem._groups || tooltipElem)[0][0] === null) {
tooltipElem = d3.select('body')
tooltipElem = d3
.select('body')
.append('div')
.attr('class', 'mermaidTooltip')
.style('opacity', 0)
.style('opacity', 0);
}
const svg = d3.select(element).select('svg')
const svg = d3.select(element).select('svg');
const nodes = svg.selectAll('g.node')
const nodes = svg.selectAll('g.node');
nodes
.on('mouseover', function () {
const el = d3.select(this)
const title = el.attr('title')
.on('mouseover', function() {
const el = d3.select(this);
const title = el.attr('title');
// Dont try to draw a tooltip if no data is provided
if (title === null) {
return
return;
}
const rect = this.getBoundingClientRect()
const rect = this.getBoundingClientRect();
tooltipElem.transition()
tooltipElem
.transition()
.duration(200)
.style('opacity', '.9')
tooltipElem.html(el.attr('title'))
.style('left', (rect.left + (rect.right - rect.left) / 2) + 'px')
.style('top', (rect.top - 14 + document.body.scrollTop) + 'px')
el.classed('hover', true)
.style('opacity', '.9');
tooltipElem
.html(el.attr('title'))
.style('left', rect.left + (rect.right - rect.left) / 2 + 'px')
.style('top', rect.top - 14 + document.body.scrollTop + 'px');
el.classed('hover', true);
})
.on('mouseout', function () {
tooltipElem.transition()
.on('mouseout', function() {
tooltipElem
.transition()
.duration(500)
.style('opacity', 0)
const el = d3.select(this)
el.classed('hover', false)
})
}
funs.push(setupToolTips)
.style('opacity', 0);
const el = d3.select(this);
el.classed('hover', false);
});
};
funs.push(setupToolTips);
/**
* Clears the internal graph db so that a new graph can be parsed.
*/
export const clear = function () {
vertices = {}
classes = {}
edges = []
funs = []
funs.push(setupToolTips)
subGraphs = []
subGraphLookup = {}
subCount = 0
tooltips = []
}
export const clear = function() {
vertices = {};
classes = {};
edges = [];
funs = [];
funs.push(setupToolTips);
subGraphs = [];
subGraphLookup = {};
subCount = 0;
tooltips = [];
firstGraphFlag = true;
};
/**
*
* @returns {string}
*/
export const defaultStyle = function () {
return 'fill:#ffa;stroke: #f66; stroke-width: 3px; stroke-dasharray: 5, 5;fill:#ffa;stroke: #666;'
}
export const defaultStyle = function() {
return 'fill:#ffa;stroke: #f66; stroke-width: 3px; stroke-dasharray: 5, 5;fill:#ffa;stroke: #666;';
};
/**
* Clears the internal graph db so that a new graph can be parsed.
*/
export const addSubGraph = function (_id, list, _title) {
let id = _id
let title = _title
export const addSubGraph = function(_id, list, _title) {
let id = _id;
let title = _title;
if (_id === _title && _title.match(/\s/)) {
id = undefined
id = undefined;
}
function uniq (a) {
const prims = { 'boolean': {}, 'number': {}, 'string': {} }
const objs = []
function uniq(a) {
const prims = { boolean: {}, number: {}, string: {} };
const objs = [];
return a.filter(function (item) {
const type = typeof item
return a.filter(function(item) {
const type = typeof item;
if (item.trim() === '') {
return false
return false;
}
if (type in prims) { return prims[type].hasOwnProperty(item) ? false : (prims[type][item] = true) } else { return objs.indexOf(item) >= 0 ? false : objs.push(item) }
})
if (type in prims) {
return prims[type].hasOwnProperty(item) ? false : (prims[type][item] = true);
} else {
return objs.indexOf(item) >= 0 ? false : objs.push(item);
}
});
}
let nodeList = []
let nodeList = [];
nodeList = uniq(nodeList.concat.apply(nodeList, list))
nodeList = uniq(nodeList.concat.apply(nodeList, list));
for (let i = 0; i < nodeList.length; i++) {
if (nodeList[i][0].match(/\d/)) nodeList[i] = 's' + nodeList[i];
}
id = id || ('subGraph' + subCount)
if (id[0].match(/\d/)) id = 's' + id
title = title || ''
title = sanitize(title)
subCount = subCount + 1
const subGraph = { id: id, nodes: nodeList, title: title.trim(), classes: [] }
subGraphs.push(subGraph)
subGraphLookup[id] = subGraph
return id
}
id = id || 'subGraph' + subCount;
if (id[0].match(/\d/)) id = 's' + id;
title = title || '';
title = sanitize(title);
subCount = subCount + 1;
const subGraph = { id: id, nodes: nodeList, title: title.trim(), classes: [] };
subGraphs.push(subGraph);
subGraphLookup[id] = subGraph;
return id;
};
const getPosForId = function (id) {
const getPosForId = function(id) {
for (let i = 0; i < subGraphs.length; i++) {
if (subGraphs[i].id === id) {
return i
return i;
}
}
return -1
}
let secCount = -1
const posCrossRef = []
const indexNodes2 = function (id, pos) {
const nodes = subGraphs[pos].nodes
secCount = secCount + 1
return -1;
};
let secCount = -1;
const posCrossRef = [];
const indexNodes2 = function(id, pos) {
const nodes = subGraphs[pos].nodes;
secCount = secCount + 1;
if (secCount > 2000) {
return
return;
}
posCrossRef[secCount] = pos
posCrossRef[secCount] = pos;
// Check if match
if (subGraphs[pos].id === id) {
return {
result: true,
count: 0
}
};
}
let count = 0
let posCount = 1
let count = 0;
let posCount = 1;
while (count < nodes.length) {
const childPos = getPosForId(nodes[count])
const childPos = getPosForId(nodes[count]);
// Ignore regular nodes (pos will be -1)
if (childPos >= 0) {
const res = indexNodes2(id, childPos)
const res = indexNodes2(id, childPos);
if (res.result) {
return {
result: true,
count: posCount + res.count
}
};
} else {
posCount = posCount + res.count
posCount = posCount + res.count;
}
}
count = count + 1
count = count + 1;
}
return {
result: false,
count: posCount
}
}
};
};
export const getDepthFirstPos = function (pos) {
return posCrossRef[pos]
}
export const indexNodes = function () {
secCount = -1
export const getDepthFirstPos = function(pos) {
return posCrossRef[pos];
};
export const indexNodes = function() {
secCount = -1;
if (subGraphs.length > 0) {
indexNodes2('none', subGraphs.length - 1, 0)
indexNodes2('none', subGraphs.length - 1, 0);
}
}
};
export const getSubGraphs = function () {
return subGraphs
}
export const getSubGraphs = function() {
return subGraphs;
};
export const firstGraph = () => {
if (firstGraphFlag) {
firstGraphFlag = false;
return true;
}
return false;
};
export default {
addVertex,
@@ -464,5 +513,8 @@ export default {
addSubGraph,
getDepthFirstPos,
indexNodes,
getSubGraphs
}
getSubGraphs,
lex: {
firstGraph
}
};

View File

@@ -1,265 +1,288 @@
import graphlib from 'graphlibrary'
import * as d3 from 'd3'
import graphlib from 'graphlibrary';
import * as d3 from 'd3';
import flowDb from './flowDb'
import flow from './parser/flow'
import { getConfig } from '../../config'
import dagreD3 from 'dagre-d3-renderer'
import addHtmlLabel from 'dagre-d3-renderer/lib/label/add-html-label.js'
import { logger } from '../../logger'
import { interpolateToCurve } from '../../utils'
import flowDb from './flowDb';
import flow from './parser/flow';
import { getConfig } from '../../config';
import dagreD3 from 'dagre-d3-renderer';
import addHtmlLabel from 'dagre-d3-renderer/lib/label/add-html-label.js';
import { logger } from '../../logger';
import { interpolateToCurve } from '../../utils';
const conf = {
}
export const setConf = function (cnf) {
const keys = Object.keys(cnf)
const conf = {};
export const setConf = function(cnf) {
const keys = Object.keys(cnf);
for (let i = 0; i < keys.length; i++) {
conf[keys[i]] = cnf[keys[i]]
conf[keys[i]] = cnf[keys[i]];
}
}
};
/**
* Function that adds the vertices found in the graph definition to the graph to be rendered.
* @param vert Object containing the vertices.
* @param g The graph that is to be drawn.
*/
export const addVertices = function (vert, g, svgId) {
const svg = d3.select(`[id="${svgId}"]`)
const keys = Object.keys(vert)
export const addVertices = function(vert, g, svgId) {
const svg = d3.select(`[id="${svgId}"]`);
const keys = Object.keys(vert);
const styleFromStyleArr = function (styleStr, arr) {
// Create a compound style definition from the style definitions found for the node in the graph definition
for (let i = 0; i < arr.length; i++) {
if (typeof arr[i] !== 'undefined') {
styleStr = styleStr + arr[i] + ';'
const styleFromStyleArr = function(styleStr, arr, { label }) {
if (!label) {
// Create a compound style definition from the style definitions found for the node in the graph definition
for (let i = 0; i < arr.length; i++) {
if (typeof arr[i] !== 'undefined') {
styleStr = styleStr + arr[i] + ';';
}
}
} else {
for (let i = 0; i < arr.length; i++) {
if (typeof arr[i] !== 'undefined') {
if (arr[i].match('^color:')) styleStr = styleStr + arr[i] + ';';
}
}
}
return styleStr
}
return styleStr;
};
// Iterate through each item in the vertex object (containing all the vertices found) in the graph definition
keys.forEach(function (id) {
const vertex = vert[id]
keys.forEach(function(id) {
const vertex = vert[id];
/**
* Variable for storing the classes for the vertex
* @type {string}
*/
let classStr = ''
let classStr = '';
if (vertex.classes.length > 0) {
classStr = vertex.classes.join(' ')
classStr = vertex.classes.join(' ');
}
/**
* Variable for storing the extracted style for the vertex
* @type {string}
*/
let style = ''
let style = '';
// Create a compound style definition from the style definitions found for the node in the graph definition
style = styleFromStyleArr(style, vertex.styles)
style = styleFromStyleArr(style, vertex.styles, { label: false });
let labelStyle = '';
labelStyle = styleFromStyleArr(labelStyle, vertex.styles, { label: true });
// Use vertex id as text in the box if no text is provided by the graph definition
let vertexText = vertex.text !== undefined ? vertex.text : vertex.id
let vertexText = vertex.text !== undefined ? vertex.text : vertex.id;
// We create a SVG label, either by delegating to addHtmlLabel or manually
let vertexNode
let vertexNode;
if (getConfig().flowchart.htmlLabels) {
// TODO: addHtmlLabel accepts a labelStyle. Do we possibly have that?
const node = { label: vertexText.replace(/fa[lrsb]?:fa-[\w-]+/g, s => `<i class='${s.replace(':', ' ')}'></i>`) }
vertexNode = addHtmlLabel(svg, node).node()
vertexNode.parentNode.removeChild(vertexNode)
const node = {
label: vertexText.replace(
/fa[lrsb]?:fa-[\w-]+/g,
s => `<i class='${s.replace(':', ' ')}'></i>`
)
};
vertexNode = addHtmlLabel(svg, node).node();
vertexNode.parentNode.removeChild(vertexNode);
} else {
const svgLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text')
const svgLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
const rows = vertexText.split(/<br[/]{0,1}>/)
const rows = vertexText.split(/<br[/]{0,1}>/);
for (let j = 0; j < rows.length; j++) {
const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan')
tspan.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve')
tspan.setAttribute('dy', '1em')
tspan.setAttribute('x', '1')
tspan.textContent = rows[j]
svgLabel.appendChild(tspan)
const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
tspan.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve');
tspan.setAttribute('dy', '1em');
tspan.setAttribute('x', '1');
tspan.textContent = rows[j];
svgLabel.appendChild(tspan);
}
vertexNode = svgLabel
vertexNode = svgLabel;
}
// If the node has a link, we wrap it in a SVG link
if (vertex.link) {
const link = document.createElementNS('http://www.w3.org/2000/svg', 'a')
link.setAttributeNS('http://www.w3.org/2000/svg', 'href', vertex.link)
link.setAttributeNS('http://www.w3.org/2000/svg', 'rel', 'noopener')
link.appendChild(vertexNode)
vertexNode = link
const link = document.createElementNS('http://www.w3.org/2000/svg', 'a');
link.setAttributeNS('http://www.w3.org/2000/svg', 'href', vertex.link);
link.setAttributeNS('http://www.w3.org/2000/svg', 'rel', 'noopener');
link.appendChild(vertexNode);
vertexNode = link;
}
let radious = 0
let _shape = ''
let radious = 0;
let _shape = '';
// Set the shape based parameters
switch (vertex.type) {
case 'round':
radious = 5
_shape = 'rect'
break
radious = 5;
_shape = 'rect';
break;
case 'square':
_shape = 'rect'
break
_shape = 'rect';
break;
case 'diamond':
_shape = 'question'
break
_shape = 'question';
break;
case 'odd':
_shape = 'rect_left_inv_arrow'
break
_shape = 'rect_left_inv_arrow';
break;
case 'lean_right':
_shape = 'lean_right'
break
_shape = 'lean_right';
break;
case 'lean_left':
_shape = 'lean_left'
break
_shape = 'lean_left';
break;
case 'trapezoid':
_shape = 'trapezoid'
break
_shape = 'trapezoid';
break;
case 'inv_trapezoid':
_shape = 'inv_trapezoid'
break
_shape = 'inv_trapezoid';
break;
case 'odd_right':
_shape = 'rect_left_inv_arrow'
break
_shape = 'rect_left_inv_arrow';
break;
case 'circle':
_shape = 'circle'
break
_shape = 'circle';
break;
case 'ellipse':
_shape = 'ellipse'
break
_shape = 'ellipse';
break;
case 'group':
_shape = 'rect'
break
_shape = 'rect';
break;
default:
_shape = 'rect'
_shape = 'rect';
}
// Add the node
g.setNode(vertex.id, { labelType: 'svg', shape: _shape, label: vertexNode, rx: radious, ry: radious, 'class': classStr, style: style, id: vertex.id })
})
}
g.setNode(vertex.id, {
labelType: 'svg',
labelStyle: labelStyle,
shape: _shape,
label: vertexNode,
rx: radious,
ry: radious,
class: classStr,
style: style,
id: vertex.id
});
});
};
/**
* Add edges to graph based on parsed graph defninition
* @param {Object} edges The edges to add to the graph
* @param {Object} g The graph object
*/
export const addEdges = function (edges, g) {
let cnt = 0
export const addEdges = function(edges, g) {
let cnt = 0;
let defaultStyle
let defaultStyle;
if (typeof edges.defaultStyle !== 'undefined') {
defaultStyle = edges.defaultStyle.toString().replace(/,/g, ';')
defaultStyle = edges.defaultStyle.toString().replace(/,/g, ';');
}
edges.forEach(function (edge) {
cnt++
const edgeData = {}
edges.forEach(function(edge) {
cnt++;
const edgeData = {};
// Set link type for rendering
if (edge.type === 'arrow_open') {
edgeData.arrowhead = 'none'
edgeData.arrowhead = 'none';
} else {
edgeData.arrowhead = 'normal'
edgeData.arrowhead = 'normal';
}
let style = ''
let style = '';
if (typeof edge.style !== 'undefined') {
edge.style.forEach(function (s) {
style = style + s + ';'
})
edge.style.forEach(function(s) {
style = style + s + ';';
});
} else {
switch (edge.stroke) {
case 'normal':
style = 'fill:none'
style = 'fill:none';
if (typeof defaultStyle !== 'undefined') {
style = defaultStyle
style = defaultStyle;
}
break
break;
case 'dotted':
style = 'stroke: #333; fill:none;stroke-width:2px;stroke-dasharray:3;'
break
style = 'stroke: #333; fill:none;stroke-width:2px;stroke-dasharray:3;';
break;
case 'thick':
style = 'stroke: #333; stroke-width: 3.5px;fill:none'
break
style = 'stroke: #333; stroke-width: 3.5px;fill:none';
break;
}
}
edgeData.style = style
edgeData.style = style;
if (typeof edge.interpolate !== 'undefined') {
edgeData.curve = interpolateToCurve(edge.interpolate, d3.curveLinear)
edgeData.curve = interpolateToCurve(edge.interpolate, d3.curveLinear);
} else if (typeof edges.defaultInterpolate !== 'undefined') {
edgeData.curve = interpolateToCurve(edges.defaultInterpolate, d3.curveLinear)
edgeData.curve = interpolateToCurve(edges.defaultInterpolate, d3.curveLinear);
} else {
edgeData.curve = interpolateToCurve(conf.curve, d3.curveLinear)
edgeData.curve = interpolateToCurve(conf.curve, d3.curveLinear);
}
if (typeof edge.text === 'undefined') {
if (typeof edge.style !== 'undefined') {
edgeData.arrowheadStyle = 'fill: #333'
edgeData.arrowheadStyle = 'fill: #333';
}
} else {
edgeData.arrowheadStyle = 'fill: #333'
edgeData.arrowheadStyle = 'fill: #333';
if (typeof edge.style === 'undefined') {
edgeData.labelpos = 'c'
edgeData.labelpos = 'c';
if (getConfig().flowchart.htmlLabels) {
edgeData.labelType = 'html'
edgeData.label = '<span class="edgeLabel">' + edge.text + '</span>'
edgeData.labelType = 'html';
edgeData.label = '<span class="edgeLabel">' + edge.text + '</span>';
} else {
edgeData.labelType = 'text'
edgeData.style = edgeData.style || 'stroke: #333; stroke-width: 1.5px;fill:none'
edgeData.label = edge.text.replace(/<br>/g, '\n')
edgeData.labelType = 'text';
edgeData.style = edgeData.style || 'stroke: #333; stroke-width: 1.5px;fill:none';
edgeData.label = edge.text.replace(/<br>/g, '\n');
}
} else {
edgeData.label = edge.text.replace(/<br>/g, '\n')
edgeData.label = edge.text.replace(/<br>/g, '\n');
}
}
// Add the edge to the graph
g.setEdge(edge.start, edge.end, edgeData, cnt)
})
}
g.setEdge(edge.start, edge.end, edgeData, cnt);
});
};
/**
* Returns the all the styles from classDef statements in the graph definition.
* @returns {object} classDef styles
*/
export const getClasses = function (text) {
logger.info('Extracting classes')
flowDb.clear()
const parser = flow.parser
parser.yy = flowDb
export const getClasses = function(text) {
logger.info('Extracting classes');
flowDb.clear();
const parser = flow.parser;
parser.yy = flowDb;
// Parse the graph definition
parser.parse(text)
return flowDb.getClasses()
}
parser.parse(text);
return flowDb.getClasses();
};
/**
* Draws a flowchart in the tag with id: id based on the graph definition in text.
* @param text
* @param id
*/
export const draw = function (text, id) {
logger.info('Drawing flowchart')
flowDb.clear()
const parser = flow.parser
parser.yy = flowDb
export const draw = function(text, id) {
logger.info('Drawing flowchart');
flowDb.clear();
const parser = flow.parser;
parser.yy = flowDb;
// Parse the graph definition
try {
parser.parse(text)
parser.parse(text);
} catch (err) {
logger.debug('Parsing failed')
logger.debug('Parsing failed');
}
// Fetch the default direction, use TD if none was found
let dir = flowDb.getDirection()
let dir = flowDb.getDirection();
if (typeof dir === 'undefined') {
dir = 'TD'
dir = 'TD';
}
// Create the input mermaid.graph
@@ -271,196 +294,238 @@ export const draw = function (text, id) {
rankdir: dir,
marginx: 20,
marginy: 20
})
.setDefaultEdgeLabel(function () {
return {}
})
.setDefaultEdgeLabel(function() {
return {};
});
let subG
const subGraphs = flowDb.getSubGraphs()
let subG;
const subGraphs = flowDb.getSubGraphs();
for (let i = subGraphs.length - 1; i >= 0; i--) {
subG = subGraphs[i]
flowDb.addVertex(subG.id, subG.title, 'group', undefined, subG.classes)
subG = subGraphs[i];
flowDb.addVertex(subG.id, subG.title, 'group', undefined, subG.classes);
}
// Fetch the verices/nodes and edges/links from the parsed graph definition
const vert = flowDb.getVertices()
const vert = flowDb.getVertices();
const edges = flowDb.getEdges()
const edges = flowDb.getEdges();
let i = 0
let i = 0;
for (i = subGraphs.length - 1; i >= 0; i--) {
subG = subGraphs[i]
subG = subGraphs[i];
d3.selectAll('cluster').append('text')
d3.selectAll('cluster').append('text');
for (let j = 0; j < subG.nodes.length; j++) {
g.setParent(subG.nodes[j], subG.id)
g.setParent(subG.nodes[j], subG.id);
}
}
addVertices(vert, g, id)
addEdges(edges, g)
addVertices(vert, g, id);
addEdges(edges, g);
// Create the renderer
const Render = dagreD3.render
const render = new Render()
const Render = dagreD3.render;
const render = new Render();
// Add custom shape for rhombus type of boc (decision)
render.shapes().question = function (parent, bbox, node) {
const w = bbox.width
const h = bbox.height
const s = (w + h) * 0.9
render.shapes().question = function(parent, bbox, node) {
const w = bbox.width;
const h = bbox.height;
const s = (w + h) * 0.9;
const points = [
{ x: s / 2, y: 0 },
{ x: s, y: -s / 2 },
{ x: s / 2, y: -s },
{ x: 0, y: -s / 2 }
]
const shapeSvg = parent.insert('polygon', ':first-child')
.attr('points', points.map(function (d) {
return d.x + ',' + d.y
}).join(' '))
];
const shapeSvg = parent
.insert('polygon', ':first-child')
.attr(
'points',
points
.map(function(d) {
return d.x + ',' + d.y;
})
.join(' ')
)
.attr('rx', 5)
.attr('ry', 5)
.attr('transform', 'translate(' + (-s / 2) + ',' + (s * 2 / 4) + ')')
node.intersect = function (point) {
return dagreD3.intersect.polygon(node, points, point)
}
return shapeSvg
}
.attr('transform', 'translate(' + -s / 2 + ',' + (s * 2) / 4 + ')');
node.intersect = function(point) {
return dagreD3.intersect.polygon(node, points, point);
};
return shapeSvg;
};
// Add custom shape for box with inverted arrow on left side
render.shapes().rect_left_inv_arrow = function (parent, bbox, node) {
const w = bbox.width
const h = bbox.height
render.shapes().rect_left_inv_arrow = function(parent, bbox, node) {
const w = bbox.width;
const h = bbox.height;
const points = [
{ x: -h / 2, y: 0 },
{ x: w, y: 0 },
{ x: w, y: -h },
{ x: -h / 2, y: -h },
{ x: 0, y: -h / 2 }
]
const shapeSvg = parent.insert('polygon', ':first-child')
.attr('points', points.map(function (d) {
return d.x + ',' + d.y
}).join(' '))
.attr('transform', 'translate(' + (-w / 2) + ',' + (h * 2 / 4) + ')')
node.intersect = function (point) {
return dagreD3.intersect.polygon(node, points, point)
}
return shapeSvg
}
];
const shapeSvg = parent
.insert('polygon', ':first-child')
.attr(
'points',
points
.map(function(d) {
return d.x + ',' + d.y;
})
.join(' ')
)
.attr('transform', 'translate(' + -w / 2 + ',' + (h * 2) / 4 + ')');
node.intersect = function(point) {
return dagreD3.intersect.polygon(node, points, point);
};
return shapeSvg;
};
// Add custom shape for box with inverted arrow on left side
render.shapes().lean_right = function (parent, bbox, node) {
const w = bbox.width
const h = bbox.height
render.shapes().lean_right = function(parent, bbox, node) {
const w = bbox.width;
const h = bbox.height;
const points = [
{ x: -2 * h / 6, y: 0 },
{ x: (-2 * h) / 6, y: 0 },
{ x: w - h / 6, y: 0 },
{ x: w + 2 * h / 6, y: -h },
{ x: w + (2 * h) / 6, y: -h },
{ x: h / 6, y: -h }
]
const shapeSvg = parent.insert('polygon', ':first-child')
.attr('points', points.map(function (d) {
return d.x + ',' + d.y
}).join(' '))
.attr('transform', 'translate(' + (-w / 2) + ',' + (h * 2 / 4) + ')')
node.intersect = function (point) {
return dagreD3.intersect.polygon(node, points, point)
}
return shapeSvg
}
];
const shapeSvg = parent
.insert('polygon', ':first-child')
.attr(
'points',
points
.map(function(d) {
return d.x + ',' + d.y;
})
.join(' ')
)
.attr('transform', 'translate(' + -w / 2 + ',' + (h * 2) / 4 + ')');
node.intersect = function(point) {
return dagreD3.intersect.polygon(node, points, point);
};
return shapeSvg;
};
// Add custom shape for box with inverted arrow on left side
render.shapes().lean_left = function (parent, bbox, node) {
const w = bbox.width
const h = bbox.height
render.shapes().lean_left = function(parent, bbox, node) {
const w = bbox.width;
const h = bbox.height;
const points = [
{ x: 2 * h / 6, y: 0 },
{ x: (2 * h) / 6, y: 0 },
{ x: w + h / 6, y: 0 },
{ x: w - 2 * h / 6, y: -h },
{ x: w - (2 * h) / 6, y: -h },
{ x: -h / 6, y: -h }
]
const shapeSvg = parent.insert('polygon', ':first-child')
.attr('points', points.map(function (d) {
return d.x + ',' + d.y
}).join(' '))
.attr('transform', 'translate(' + (-w / 2) + ',' + (h * 2 / 4) + ')')
node.intersect = function (point) {
return dagreD3.intersect.polygon(node, points, point)
}
return shapeSvg
}
];
const shapeSvg = parent
.insert('polygon', ':first-child')
.attr(
'points',
points
.map(function(d) {
return d.x + ',' + d.y;
})
.join(' ')
)
.attr('transform', 'translate(' + -w / 2 + ',' + (h * 2) / 4 + ')');
node.intersect = function(point) {
return dagreD3.intersect.polygon(node, points, point);
};
return shapeSvg;
};
// Add custom shape for box with inverted arrow on left side
render.shapes().trapezoid = function (parent, bbox, node) {
const w = bbox.width
const h = bbox.height
render.shapes().trapezoid = function(parent, bbox, node) {
const w = bbox.width;
const h = bbox.height;
const points = [
{ x: -2 * h / 6, y: 0 },
{ x: w + 2 * h / 6, y: 0 },
{ x: (-2 * h) / 6, y: 0 },
{ x: w + (2 * h) / 6, y: 0 },
{ x: w - h / 6, y: -h },
{ x: h / 6, y: -h }
]
const shapeSvg = parent.insert('polygon', ':first-child')
.attr('points', points.map(function (d) {
return d.x + ',' + d.y
}).join(' '))
.attr('transform', 'translate(' + (-w / 2) + ',' + (h * 2 / 4) + ')')
node.intersect = function (point) {
return dagreD3.intersect.polygon(node, points, point)
}
return shapeSvg
}
];
const shapeSvg = parent
.insert('polygon', ':first-child')
.attr(
'points',
points
.map(function(d) {
return d.x + ',' + d.y;
})
.join(' ')
)
.attr('transform', 'translate(' + -w / 2 + ',' + (h * 2) / 4 + ')');
node.intersect = function(point) {
return dagreD3.intersect.polygon(node, points, point);
};
return shapeSvg;
};
// Add custom shape for box with inverted arrow on left side
render.shapes().inv_trapezoid = function (parent, bbox, node) {
const w = bbox.width
const h = bbox.height
render.shapes().inv_trapezoid = function(parent, bbox, node) {
const w = bbox.width;
const h = bbox.height;
const points = [
{ x: h / 6, y: 0 },
{ x: w - h / 6, y: 0 },
{ x: w + 2 * h / 6, y: -h },
{ x: -2 * h / 6, y: -h }
]
const shapeSvg = parent.insert('polygon', ':first-child')
.attr('points', points.map(function (d) {
return d.x + ',' + d.y
}).join(' '))
.attr('transform', 'translate(' + (-w / 2) + ',' + (h * 2 / 4) + ')')
node.intersect = function (point) {
return dagreD3.intersect.polygon(node, points, point)
}
return shapeSvg
}
{ x: w + (2 * h) / 6, y: -h },
{ x: (-2 * h) / 6, y: -h }
];
const shapeSvg = parent
.insert('polygon', ':first-child')
.attr(
'points',
points
.map(function(d) {
return d.x + ',' + d.y;
})
.join(' ')
)
.attr('transform', 'translate(' + -w / 2 + ',' + (h * 2) / 4 + ')');
node.intersect = function(point) {
return dagreD3.intersect.polygon(node, points, point);
};
return shapeSvg;
};
// Add custom shape for box with inverted arrow on right side
render.shapes().rect_right_inv_arrow = function (parent, bbox, node) {
const w = bbox.width
const h = bbox.height
render.shapes().rect_right_inv_arrow = function(parent, bbox, node) {
const w = bbox.width;
const h = bbox.height;
const points = [
{ x: 0, y: 0 },
{ x: w + h / 2, y: 0 },
{ x: w, y: -h / 2 },
{ x: w + h / 2, y: -h },
{ x: 0, y: -h }
]
const shapeSvg = parent.insert('polygon', ':first-child')
.attr('points', points.map(function (d) {
return d.x + ',' + d.y
}).join(' '))
.attr('transform', 'translate(' + (-w / 2) + ',' + (h * 2 / 4) + ')')
node.intersect = function (point) {
return dagreD3.intersect.polygon(node, points, point)
}
return shapeSvg
}
];
const shapeSvg = parent
.insert('polygon', ':first-child')
.attr(
'points',
points
.map(function(d) {
return d.x + ',' + d.y;
})
.join(' ')
)
.attr('transform', 'translate(' + -w / 2 + ',' + (h * 2) / 4 + ')');
node.intersect = function(point) {
return dagreD3.intersect.polygon(node, points, point);
};
return shapeSvg;
};
// Add our custom arrow - an empty arrowhead
render.arrows().none = function normal (parent, id, edge, type) {
const marker = parent.append('marker')
render.arrows().none = function normal(parent, id, edge, type) {
const marker = parent
.append('marker')
.attr('id', id)
.attr('viewBox', '0 0 10 10')
.attr('refX', 9)
@@ -468,16 +533,16 @@ export const draw = function (text, id) {
.attr('markerUnits', 'strokeWidth')
.attr('markerWidth', 8)
.attr('markerHeight', 6)
.attr('orient', 'auto')
.attr('orient', 'auto');
const path = marker.append('path')
.attr('d', 'M 0 0 L 0 0 L 0 0 z')
dagreD3.util.applyStyle(path, edge[type + 'Style'])
}
const path = marker.append('path').attr('d', 'M 0 0 L 0 0 L 0 0 z');
dagreD3.util.applyStyle(path, edge[type + 'Style']);
};
// Override normal arrowhead defined in d3. Remove style & add class to allow css styling.
render.arrows().normal = function normal (parent, id, edge, type) {
const marker = parent.append('marker')
render.arrows().normal = function normal(parent, id, edge, type) {
const marker = parent
.append('marker')
.attr('id', id)
.attr('viewBox', '0 0 10 10')
.attr('refX', 9)
@@ -485,76 +550,76 @@ export const draw = function (text, id) {
.attr('markerUnits', 'strokeWidth')
.attr('markerWidth', 8)
.attr('markerHeight', 6)
.attr('orient', 'auto')
.attr('orient', 'auto');
marker.append('path')
marker
.append('path')
.attr('d', 'M 0 0 L 10 5 L 0 10 z')
.attr('class', 'arrowheadPath')
.style('stroke-width', 1)
.style('stroke-dasharray', '1,0')
}
.style('stroke-dasharray', '1,0');
};
// Set up an SVG group so that we can translate the final graph.
const svg = d3.select(`[id="${id}"]`)
const svg = d3.select(`[id="${id}"]`);
// Run the renderer. This is what draws the final graph.
const element = d3.select('#' + id + ' g')
render(element, g)
const element = d3.select('#' + id + ' g');
render(element, g);
element.selectAll('g.node')
.attr('title', function () {
return flowDb.getTooltip(this.id)
})
element.selectAll('g.node').attr('title', function() {
return flowDb.getTooltip(this.id);
});
const padding = 8
const width = g.maxX - g.minX + padding * 2
const height = g.maxY - g.minY + padding * 2
svg.attr('width', '100%')
svg.attr('style', `max-width: ${width}px;`)
svg.attr('viewBox', `0 0 ${width} ${height}`)
svg.select('g').attr('transform', `translate(${padding - g.minX}, ${padding - g.minY})`)
const padding = 8;
const width = g.maxX - g.minX + padding * 2;
const height = g.maxY - g.minY + padding * 2;
svg.attr('width', '100%');
svg.attr('style', `max-width: ${width}px;`);
svg.attr('viewBox', `0 0 ${width} ${height}`);
svg.select('g').attr('transform', `translate(${padding - g.minX}, ${padding - g.minY})`);
// Index nodes
flowDb.indexNodes('subGraph' + i)
flowDb.indexNodes('subGraph' + i);
// reposition labels
for (i = 0; i < subGraphs.length; i++) {
subG = subGraphs[i]
subG = subGraphs[i];
if (subG.title !== 'undefined') {
const clusterRects = document.querySelectorAll('#' + id + ' #' + subG.id + ' rect')
const clusterEl = document.querySelectorAll('#' + id + ' #' + subG.id)
const clusterRects = document.querySelectorAll('#' + id + ' #' + subG.id + ' rect');
const clusterEl = document.querySelectorAll('#' + id + ' #' + subG.id);
const xPos = clusterRects[0].x.baseVal.value
const yPos = clusterRects[0].y.baseVal.value
const width = clusterRects[0].width.baseVal.value
const cluster = d3.select(clusterEl[0])
const te = cluster.select('.label')
te.attr('transform', `translate(${xPos + width / 2}, ${yPos + 14})`)
te.attr('id', id + 'Text')
const xPos = clusterRects[0].x.baseVal.value;
const yPos = clusterRects[0].y.baseVal.value;
const width = clusterRects[0].width.baseVal.value;
const cluster = d3.select(clusterEl[0]);
const te = cluster.select('.label');
te.attr('transform', `translate(${xPos + width / 2}, ${yPos + 14})`);
te.attr('id', id + 'Text');
}
}
// Add label rects for non html labels
if (!getConfig().flowchart.htmlLabels) {
const labels = document.querySelectorAll('#' + id + ' .edgeLabel .label')
const labels = document.querySelectorAll('#' + id + ' .edgeLabel .label');
for (let k = 0; k < labels.length; k++) {
const label = labels[k]
const label = labels[k];
// Get dimensions of label
const dim = label.getBBox()
const dim = label.getBBox();
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
rect.setAttribute('rx', 0)
rect.setAttribute('ry', 0)
rect.setAttribute('width', dim.width)
rect.setAttribute('height', dim.height)
rect.setAttribute('style', 'fill:#e8e8e8;')
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
rect.setAttribute('rx', 0);
rect.setAttribute('ry', 0);
rect.setAttribute('width', dim.width);
rect.setAttribute('height', dim.height);
rect.setAttribute('style', 'fill:#e8e8e8;');
label.insertBefore(rect, label.firstChild)
label.insertBefore(rect, label.firstChild);
}
}
}
};
export default {
setConf,
@@ -562,4 +627,4 @@ export default {
addEdges,
getClasses,
draw
}
};

View File

@@ -0,0 +1,37 @@
import flowDb from '../flowDb';
import flow from './flow';
import { setConfig } from '../../../config';
setConfig({
securityLevel: 'strict'
});
describe('when parsing flowcharts', function() {
beforeEach(function() {
flow.parser.yy = flowDb;
flow.parser.yy.clear();
});
it('should handle chaining of vertices', function() {
const res = flow.parser.parse(`
graph TD
A-->B-->C;
`);
const vert = flow.parser.yy.getVertices();
const edges = flow.parser.yy.getEdges();
expect(vert['A'].id).toBe('A');
expect(vert['B'].id).toBe('B');
expect(vert['C'].id).toBe('C');
expect(edges.length).toBe(2);
expect(edges[0].start).toBe('A');
expect(edges[0].end).toBe('B');
expect(edges[0].type).toBe('arrow');
expect(edges[0].text).toBe('');
expect(edges[1].start).toBe('B');
expect(edges[1].end).toBe('C');
expect(edges[1].type).toBe('arrow');
expect(edges[1].text).toBe('');
});
});

Some files were not shown because too many files have changed in this diff Show More