Compare commits

..

1 Commits

Author SHA1 Message Date
Sidharth Vinod
bbff96ef56 Dummy Commit 2025-07-10 01:02:11 +05:30
39 changed files with 1745 additions and 2692 deletions

View File

@@ -1,7 +0,0 @@
---
'@mermaid-js/examples': patch
'mermaid': patch
'@mermaid-js/parser': patch
---
chore: Move packet diagram out of beta

View File

@@ -1,5 +0,0 @@
---
'mermaid': patch
---
fix: adjust sequence diagram title positioning to prevent overlap with top border in Safari

View File

@@ -1,7 +0,0 @@
---
'mermaid': patch
---
fix(timeline): fix loading `leftMargin` from config
The `timeline.leftMargin` config value should now correctly control the size of the left margin, instead of being ignored.

View File

@@ -42,4 +42,4 @@ jobs:
working-directory: ./packages/mermaid
run: pnpm run docs:build
- uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27 # main
- uses: autofix-ci/action@551dded8c6cc8a1054039c8bc0b8b48c51dfc6ef # main

View File

@@ -42,7 +42,7 @@ jobs:
pull_number: prNumber,
});
const isSponsored = commits.every(
const isSponsored = commits.some(
(c) => c.commit.author.email?.endsWith('@mermaidchart.com')
);

13
__mocks__/d3.ts Normal file
View File

@@ -0,0 +1,13 @@
import { MockedD3 } from '../packages/mermaid/src/tests/MockedD3.js';
export const select = function () {
return new MockedD3();
};
export const selectAll = function () {
return new MockedD3();
};
export const curveBasis = 'basis';
export const curveLinear = 'linear';
export const curveCardinal = 'cardinal';

View File

@@ -26,10 +26,7 @@ export default eyesPlugin(
config.env.useArgos = process.env.RUN_VISUAL_TEST === 'true';
if (config.env.useArgos) {
registerArgosTask(on, config, {
// Enable upload to Argos only when it runs on CI.
uploadToArgos: !!process.env.CI,
});
registerArgosTask(on, config);
} else {
addMatchImageSnapshotPlugin(on, config);
}

View File

@@ -1,7 +1,7 @@
import { imgSnapshotTest } from '../../helpers/util';
describe('packet structure', () => {
it('should render a simple packet-beta diagram', () => {
it('should render a simple packet diagram', () => {
imgSnapshotTest(
`packet-beta
title Hello world
@@ -10,18 +10,9 @@ describe('packet structure', () => {
);
});
it('should render a simple packet diagram', () => {
imgSnapshotTest(
`packet
title Hello world
0-10: "hello"
`
);
});
it('should render a simple packet diagram without ranges', () => {
imgSnapshotTest(
`packet
`packet-beta
0: "h"
1: "i"
`
@@ -30,7 +21,7 @@ describe('packet structure', () => {
it('should render a complex packet diagram', () => {
imgSnapshotTest(
`packet
`packet-beta
0-15: "Source Port"
16-31: "Destination Port"
32-63: "Sequence Number"
@@ -61,7 +52,7 @@ describe('packet structure', () => {
packet:
showBits: false
---
packet
packet-beta
0-15: "Source Port"
16-31: "Destination Port"
32-63: "Sequence Number"

View File

@@ -75,6 +75,7 @@
end
D --> D
end
C1 --> C1
end
</pre>

View File

@@ -4,7 +4,7 @@
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/font-awesome.min.css"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"
/>
<link
href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css"

View File

@@ -17,7 +17,7 @@
<div class="diagrams">
<pre class="mermaid">
packet
packet-beta
0-15: "Source Port"
16-31: "Destination Port"
32-63: "Sequence Number"
@@ -44,7 +44,7 @@
packet:
showBits: false
---
packet
packet-beta
0-15: "Source Port"
16-31: "Destination Port"
32-63: "Sequence Number"
@@ -70,7 +70,7 @@
config:
theme: forest
---
packet
packet-beta
title Forest theme
0-15: "Source Port"
16-31: "Destination Port"
@@ -97,7 +97,7 @@
config:
theme: dark
---
packet
packet-beta
title Dark theme
0-15: "Source Port"
16-31: "Destination Port"

View File

@@ -301,7 +301,7 @@ If you are adding a feature, you will definitely need to add tests. Depending on
Unit tests are tests that test a single function or module. They are the easiest to write and the fastest to run.
Unit tests are mandatory for all code except the layout tests. (The layouts are tested with integration tests.)
Unit tests are mandatory for all code except the renderers. (The renderers are tested with integration tests.)
We use [Vitest](https://vitest.dev) to run unit tests.
@@ -327,30 +327,6 @@ When using Docker prepend your command with `./run`:
./run pnpm test
```
##### Testing the DOM
One can use `jsdomIt` to test any part of Mermaid that interacts with the DOM, as long as it is not related to the layout.
The function `jsdomIt` ([developed in utils.ts](../../tests/util.ts)) overrides `it` from `vitest`, and creates a pseudo-browser environment that works almost like the real deal for the duration of the test. It uses JSDOM to create a DOM, and adds objects `window` and `document` to `global` to mock the browser environment.
> \[!NOTE]
> The layout cannot work in `jsdomIt` tests because JSDOM has no rendering engine, hence the pseudo-browser environment.
Example :
```typescript
import { ensureNodeFromSelector, jsdomIt } from './tests/util.js';
jsdomIt('should add element "thing" in the SVG', ({ svg }) => {
// Code in this block runs in a pseudo-browser environment
addThing(svg); // The svg item is the D3 selection of the SVG node
const svgNode = ensureNodeFromSelector('svg'); // Retrieve the DOM node using the DOM API
expect(svgNode.querySelector('thing')).not.toBeNull(); // Test the structure of the SVG
});
```
They can be used to test any method that interacts with the DOM, including for testing renderers. For renderers, additional integration testing is necessary to test the layout though.
#### Integration / End-to-End (E2E) Tests
These test the rendering and visual appearance of the diagrams.

View File

@@ -17,7 +17,7 @@ This diagram type is particularly useful for developers, network engineers, educ
## Syntax
```
packet
packet-beta
start: "Block name" %% Single-bit block
start-end: "Block name" %% Multi-bit blocks
... More Fields ...
@@ -28,7 +28,7 @@ start-end: "Block name" %% Multi-bit blocks
Using start and end bit counts can be difficult, especially when modifying a design. For this we add a bit count field, which starts from the end of the previous field automagically. Use `+<count>` to set the number of bits, thus:
```
packet
packet-beta
+1: "Block name" %% Single-bit block
+8: "Block name" %% 8-bit block
9-15: "Manually set start and end, it's fine to mix and match"
@@ -41,7 +41,7 @@ packet
---
title: "TCP Packet"
---
packet
packet-beta
0-15: "Source Port"
16-31: "Destination Port"
32-63: "Sequence Number"
@@ -65,7 +65,7 @@ packet
---
title: "TCP Packet"
---
packet
packet-beta
0-15: "Source Port"
16-31: "Destination Port"
32-63: "Sequence Number"
@@ -86,7 +86,7 @@ packet
```
```mermaid-example
packet
packet-beta
title UDP Packet
+16: "Source Port"
+16: "Destination Port"
@@ -96,7 +96,7 @@ title UDP Packet
```
```mermaid
packet
packet-beta
title UDP Packet
+16: "Source Port"
+16: "Destination Port"
@@ -144,7 +144,7 @@ config:
packet:
startByteColor: red
---
packet
packet-beta
0-15: "Source Port"
16-31: "Destination Port"
32-63: "Sequence Number"

0
dummy.txt Normal file
View File

View File

@@ -64,7 +64,7 @@
},
"devDependencies": {
"@applitools/eyes-cypress": "^3.44.9",
"@argos-ci/cypress": "^5.0.2",
"@argos-ci/cypress": "^4.0.3",
"@changesets/changelog-github": "^0.5.1",
"@changesets/cli": "^2.27.12",
"@cspell/eslint-plugin": "^8.19.3",
@@ -88,8 +88,8 @@
"cors": "^2.8.5",
"cpy-cli": "^5.0.0",
"cross-env": "^7.0.3",
"cspell": "^9.1.3",
"cypress": "^14.5.1",
"cspell": "^8.6.1",
"cypress": "^14.0.3",
"cypress-image-snapshot": "^4.0.1",
"cypress-split": "^1.24.14",
"esbuild": "^0.25.0",
@@ -109,25 +109,25 @@
"globals": "^16.0.0",
"globby": "^14.0.2",
"husky": "^9.1.7",
"jest": "^30.0.4",
"jest": "^29.7.0",
"jison": "^0.4.18",
"js-yaml": "^4.1.0",
"jsdom": "^26.1.0",
"jsdom": "^26.0.0",
"langium-cli": "3.3.0",
"lint-staged": "^16.1.2",
"lint-staged": "^15.2.11",
"markdown-table": "^3.0.4",
"nyc": "^17.1.0",
"path-browserify": "^1.0.1",
"prettier": "^3.5.2",
"prettier-plugin-jsdoc": "^1.3.2",
"rimraf": "^6.0.1",
"rollup-plugin-visualizer": "^6.0.3",
"rollup-plugin-visualizer": "^5.14.0",
"start-server-and-test": "^2.0.10",
"tslib": "^2.8.1",
"tsx": "^4.7.3",
"typescript": "~5.7.3",
"typescript-eslint": "^8.32.0",
"vite": "^7.0.3",
"vite": "^6.1.1",
"vite-plugin-istanbul": "^7.0.0",
"vitest": "^3.0.6"
},
@@ -139,13 +139,8 @@
"roughjs": "patches/roughjs.patch"
},
"onlyBuiltDependencies": [
"canvas",
"cypress",
"esbuild"
],
"ignoredBuiltDependencies": [
"sharp",
"vue-demi"
]
}
}

View File

@@ -11,7 +11,7 @@ export default {
code: `---
title: "TCP Packet"
---
packet
packet-beta
0-15: "Source Port"
16-31: "Destination Port"
32-63: "Sequence Number"

View File

@@ -82,7 +82,7 @@
"katex": "^0.16.9",
"khroma": "^2.1.0",
"lodash-es": "^4.17.21",
"marked": "^16.0.0",
"marked": "^15.0.7",
"roughjs": "^4.6.6",
"stylis": "^4.3.6",
"ts-dedent": "^2.2.0",
@@ -105,14 +105,13 @@
"@types/stylis": "^4.2.7",
"@types/uuid": "^10.0.0",
"ajv": "^8.17.1",
"canvas": "^3.1.0",
"chokidar": "3.6.0",
"concurrently": "^9.1.2",
"csstree-validator": "^4.0.1",
"globby": "^14.0.2",
"jison": "^0.4.18",
"js-base64": "^3.7.7",
"jsdom": "^26.1.0",
"jsdom": "^26.0.0",
"json-schema-to-typescript": "^15.0.4",
"micromatch": "^4.0.8",
"path-browserify": "^1.0.1",

View File

@@ -1,25 +1,28 @@
import { addSVGa11yTitleDescription, setA11yDiagramInfo } from './accessibility.js';
import { ensureNodeFromSelector, jsdomIt } from './tests/util.js';
import { expect } from 'vitest';
import { MockedD3 } from './tests/MockedD3.js';
import { setA11yDiagramInfo, addSVGa11yTitleDescription } from './accessibility.js';
import type { D3Element } from './types.js';
describe('accessibility', () => {
const fauxSvgNode: MockedD3 = new MockedD3();
describe('setA11yDiagramInfo', () => {
jsdomIt('should set svg element role to "graphics-document document"', ({ svg }) => {
setA11yDiagramInfo(svg, 'flowchart');
const svgNode = ensureNodeFromSelector('svg');
expect(svgNode.getAttribute('role')).toBe('graphics-document document');
it('should set svg element role to "graphics-document document"', () => {
const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
setA11yDiagramInfo(fauxSvgNode, 'flowchart');
expect(svgAttrSpy).toHaveBeenCalledWith('role', 'graphics-document document');
});
jsdomIt('should set aria-roledescription to the diagram type', ({ svg }) => {
setA11yDiagramInfo(svg, 'flowchart');
const svgNode = ensureNodeFromSelector('svg');
expect(svgNode.getAttribute('aria-roledescription')).toBe('flowchart');
it('should set aria-roledescription to the diagram type', () => {
const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
setA11yDiagramInfo(fauxSvgNode, 'flowchart');
expect(svgAttrSpy).toHaveBeenCalledWith('aria-roledescription', 'flowchart');
});
jsdomIt('should not set aria-roledescription if the diagram type is empty', ({ svg }) => {
setA11yDiagramInfo(svg, '');
const svgNode = ensureNodeFromSelector('svg');
expect(svgNode.getAttribute('aria-roledescription')).toBeNull();
it('should not set aria-roledescription if the diagram type is empty', () => {
const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
setA11yDiagramInfo(fauxSvgNode, '');
expect(svgAttrSpy).toHaveBeenCalledTimes(1);
expect(svgAttrSpy).toHaveBeenCalledWith('role', expect.anything()); // only called to set the role
});
});
@@ -36,78 +39,115 @@ describe('accessibility', () => {
expect(noInsertAttrSpy).not.toHaveBeenCalled();
});
// convenience functions to DRY up the spec
function expectAriaLabelledByItTitleId(
svgD3Node: D3Element,
title: string | undefined,
desc: string | undefined,
givenId: string
): void {
const svgAttrSpy = vi.spyOn(svgD3Node, 'attr').mockReturnValue(svgD3Node);
addSVGa11yTitleDescription(svgD3Node, title, desc, givenId);
expect(svgAttrSpy).toHaveBeenCalledWith('aria-labelledby', `chart-title-${givenId}`);
}
function expectAriaDescribedByItDescId(
svgD3Node: D3Element,
title: string | undefined,
desc: string | undefined,
givenId: string
): void {
const svgAttrSpy = vi.spyOn(svgD3Node, 'attr').mockReturnValue(svgD3Node);
addSVGa11yTitleDescription(svgD3Node, title, desc, givenId);
expect(svgAttrSpy).toHaveBeenCalledWith('aria-describedby', `chart-desc-${givenId}`);
}
function a11yTitleTagInserted(
svgD3Node: D3Element,
title: string | undefined,
desc: string | undefined,
givenId: string,
callNumber: number
): void {
a11yTagInserted(svgD3Node, title, desc, givenId, callNumber, 'title', title);
}
function a11yDescTagInserted(
svgD3Node: D3Element,
title: string | undefined,
desc: string | undefined,
givenId: string,
callNumber: number
): void {
a11yTagInserted(svgD3Node, title, desc, givenId, callNumber, 'desc', desc);
}
function a11yTagInserted(
_svgD3Node: D3Element,
title: string | undefined,
desc: string | undefined,
givenId: string,
callNumber: number,
expectedPrefix: string,
expectedText: string | undefined
): void {
const fauxInsertedD3: MockedD3 = new MockedD3();
const svginsertpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxInsertedD3);
const titleAttrSpy = vi.spyOn(fauxInsertedD3, 'attr').mockReturnValue(fauxInsertedD3);
const titleTextSpy = vi.spyOn(fauxInsertedD3, 'text');
addSVGa11yTitleDescription(fauxSvgNode, title, desc, givenId);
expect(svginsertpy).toHaveBeenCalledWith(expectedPrefix, ':first-child');
expect(titleAttrSpy).toHaveBeenCalledWith('id', `chart-${expectedPrefix}-${givenId}`);
expect(titleTextSpy).toHaveBeenNthCalledWith(callNumber, expectedText);
}
describe('with a11y title', () => {
const a11yTitle = 'a11y title';
describe('with a11y description', () => {
const a11yDesc = 'a11y description';
jsdomIt('should set aria-labelledby to the title id inserted as a child', ({ svg }) => {
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
const svgNode = ensureNodeFromSelector('svg');
expect(svgNode.getAttribute('aria-labelledby')).toBe(`chart-title-${givenId}`);
it('should set aria-labelledby to the title id inserted as a child', () => {
expectAriaLabelledByItTitleId(fauxSvgNode, a11yTitle, a11yDesc, givenId);
});
jsdomIt(
'should set aria-describedby to the description id inserted as a child',
({ svg }) => {
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
const svgNode = ensureNodeFromSelector('svg');
expect(svgNode.getAttribute('aria-describedby')).toBe(`chart-desc-${givenId}`);
}
);
it('should set aria-describedby to the description id inserted as a child', () => {
expectAriaDescribedByItDescId(fauxSvgNode, a11yTitle, a11yDesc, givenId);
});
jsdomIt(
'should insert title tag as the first child with the text set to the accTitle given',
({ svg }) => {
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
const svgNode = ensureNodeFromSelector('svg');
const titleNode = ensureNodeFromSelector('title', svgNode);
expect(titleNode?.innerHTML).toBe(a11yTitle);
}
);
it('should insert title tag as the first child with the text set to the accTitle given', () => {
a11yTitleTagInserted(fauxSvgNode, a11yTitle, a11yDesc, givenId, 2);
});
jsdomIt(
'should insert desc tag as the 2nd child with the text set to accDescription given',
({ svg }) => {
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
const svgNode = ensureNodeFromSelector('svg');
const descNode = ensureNodeFromSelector('desc', svgNode);
expect(descNode?.innerHTML).toBe(a11yDesc);
}
);
it('should insert desc tag as the 2nd child with the text set to accDescription given', () => {
a11yDescTagInserted(fauxSvgNode, a11yTitle, a11yDesc, givenId, 1);
});
});
describe(`without a11y description`, {}, () => {
describe(`without a11y description`, () => {
const a11yDesc = undefined;
jsdomIt('should set aria-labelledby to the title id inserted as a child', ({ svg }) => {
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
const svgNode = ensureNodeFromSelector('svg');
expect(svgNode.getAttribute('aria-labelledby')).toBe(`chart-title-${givenId}`);
it('should set aria-labelledby to the title id inserted as a child', () => {
expectAriaLabelledByItTitleId(fauxSvgNode, a11yTitle, a11yDesc, givenId);
});
jsdomIt('should not set aria-describedby', ({ svg }) => {
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
const svgNode = ensureNodeFromSelector('svg');
expect(svgNode.getAttribute('aria-describedby')).toBeNull();
it('should not set aria-describedby', () => {
const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
expect(svgAttrSpy).not.toHaveBeenCalledWith('aria-describedby', expect.anything());
});
jsdomIt(
'should insert title tag as the first child with the text set to the accTitle given',
({ svg }) => {
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
const svgNode = ensureNodeFromSelector('svg');
const titleNode = ensureNodeFromSelector('title', svgNode);
expect(titleNode?.innerHTML).toBe(a11yTitle);
}
);
it('should insert title tag as the first child with the text set to the accTitle given', () => {
a11yTitleTagInserted(fauxSvgNode, a11yTitle, a11yDesc, givenId, 1);
});
jsdomIt('should not insert description tag', ({ svg }) => {
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
const svgNode = ensureNodeFromSelector('svg');
const descNode = svgNode.querySelector('desc');
expect(descNode).toBeNull();
it('should not insert description tag', () => {
const fauxTitle: MockedD3 = new MockedD3();
const svginsertpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxTitle);
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
expect(svginsertpy).not.toHaveBeenCalledWith('desc', ':first-child');
});
});
});
@@ -118,66 +158,55 @@ describe('accessibility', () => {
describe('with a11y description', () => {
const a11yDesc = 'a11y description';
jsdomIt('should not set aria-labelledby', ({ svg }) => {
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
const svgNode = ensureNodeFromSelector('svg');
expect(svgNode.getAttribute('aria-labelledby')).toBeNull();
it('should not set aria-labelledby', () => {
const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
expect(svgAttrSpy).not.toHaveBeenCalledWith('aria-labelledby', expect.anything());
});
jsdomIt('should not insert title tag', ({ svg }) => {
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
const svgNode = ensureNodeFromSelector('svg');
const titleNode = svgNode.querySelector('title');
expect(titleNode).toBeNull();
it('should not insert title tag', () => {
const fauxTitle: MockedD3 = new MockedD3();
const svginsertpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxTitle);
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
expect(svginsertpy).not.toHaveBeenCalledWith('title', ':first-child');
});
jsdomIt(
'should set aria-describedby to the description id inserted as a child',
({ svg }) => {
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
const svgNode = ensureNodeFromSelector('svg');
expect(svgNode.getAttribute('aria-describedby')).toBe(`chart-desc-${givenId}`);
}
);
it('should set aria-describedby to the description id inserted as a child', () => {
expectAriaDescribedByItDescId(fauxSvgNode, a11yTitle, a11yDesc, givenId);
});
jsdomIt(
'should insert desc tag as the 2nd child with the text set to accDescription given',
({ svg }) => {
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
const svgNode = ensureNodeFromSelector('svg');
const descNode = ensureNodeFromSelector('desc', svgNode);
expect(descNode?.innerHTML).toBe(a11yDesc);
}
);
it('should insert desc tag as the 2nd child with the text set to accDescription given', () => {
a11yDescTagInserted(fauxSvgNode, a11yTitle, a11yDesc, givenId, 1);
});
});
describe('without a11y description', () => {
const a11yDesc = undefined;
jsdomIt('should not set aria-labelledby', ({ svg }) => {
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
const svgNode = ensureNodeFromSelector('svg');
expect(svgNode.getAttribute('aria-labelledby')).toBeNull();
it('should not set aria-labelledby', () => {
const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
expect(svgAttrSpy).not.toHaveBeenCalledWith('aria-labelledby', expect.anything());
});
jsdomIt('should not set aria-describedby', ({ svg }) => {
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
const svgNode = ensureNodeFromSelector('svg');
expect(svgNode.getAttribute('aria-describedby')).toBeNull();
it('should not set aria-describedby', () => {
const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
expect(svgAttrSpy).not.toHaveBeenCalledWith('aria-describedby', expect.anything());
});
jsdomIt('should not insert title tag', ({ svg }) => {
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
const svgNode = ensureNodeFromSelector('svg');
const titleNode = svgNode.querySelector('title');
expect(titleNode).toBeNull();
it('should not insert title tag', () => {
const fauxTitle: MockedD3 = new MockedD3();
const svginsertpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxTitle);
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
expect(svginsertpy).not.toHaveBeenCalledWith('title', ':first-child');
});
jsdomIt('should not insert description tag', ({ svg }) => {
addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId);
const svgNode = ensureNodeFromSelector('svg');
const descNode = svgNode.querySelector('desc');
expect(descNode).toBeNull();
it('should not insert description tag', () => {
const fauxDesc: MockedD3 = new MockedD3();
const svginsertpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxDesc);
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
expect(svginsertpy).not.toHaveBeenCalledWith('desc', ':first-child');
});
});
});

View File

@@ -379,15 +379,6 @@ function layoutArchitecture(
},
},
],
layout: {
name: 'grid',
boundingBox: {
x1: 0,
x2: 100,
y1: 0,
y2: 100,
},
},
});
// Remove element after layout
renderEl.remove();

View File

@@ -287,7 +287,7 @@ const setBlock = (block: Block) => {
blockDatabase.set(block.id, block);
};
const getLogger = () => log;
const getLogger = () => console;
/**
* Return all of the style classes

View File

@@ -7,7 +7,7 @@ import type {
const id = 'packet';
const detector: DiagramDetector = (txt) => {
return /^\s*packet(-beta)?/.test(txt);
return /^\s*packet-beta/.test(txt);
};
const loader: DiagramLoader = async () => {

View File

@@ -15,14 +15,8 @@ describe('packet diagrams', () => {
expect(getPacket()).toMatchInlineSnapshot('[]');
});
it('should handle a packet definition', async () => {
const str = `packet`;
await expect(parser.parse(str)).resolves.not.toThrow();
expect(getPacket()).toMatchInlineSnapshot('[]');
});
it('should handle diagram with data and title', async () => {
const str = `packet
const str = `packet-beta
title Packet diagram
accTitle: Packet accTitle
accDescr: Packet accDescription
@@ -47,7 +41,7 @@ describe('packet diagrams', () => {
});
it('should handle single bits', async () => {
const str = `packet
const str = `packet-beta
0-10: "test"
11: "single"
`;
@@ -73,7 +67,7 @@ describe('packet diagrams', () => {
});
it('should handle bit counts', async () => {
const str = `packet
const str = `packet-beta
+8: "byte"
+16: "word"
`;
@@ -99,7 +93,7 @@ describe('packet diagrams', () => {
});
it('should handle bit counts with bit or bits', async () => {
const str = `packet
const str = `packet-beta
+8: "byte"
+16: "word"
`;
@@ -125,7 +119,7 @@ describe('packet diagrams', () => {
});
it('should split into multiple rows', async () => {
const str = `packet
const str = `packet-beta
0-10: "test"
11-90: "multiple"
`;
@@ -167,7 +161,7 @@ describe('packet diagrams', () => {
});
it('should split into multiple rows when cut at exact length', async () => {
const str = `packet
const str = `packet-beta
0-16: "test"
17-63: "multiple"
`;
@@ -201,7 +195,7 @@ describe('packet diagrams', () => {
});
it('should throw error if numbers are not continuous', async () => {
const str = `packet
const str = `packet-beta
0-16: "test"
18-20: "error"
`;
@@ -211,7 +205,7 @@ describe('packet diagrams', () => {
});
it('should throw error if numbers are not continuous with bit counts', async () => {
const str = `packet
const str = `packet-beta
+16: "test"
18-20: "error"
`;
@@ -221,7 +215,7 @@ describe('packet diagrams', () => {
});
it('should throw error if numbers are not continuous for single packets', async () => {
const str = `packet
const str = `packet-beta
0-16: "test"
18: "error"
`;
@@ -231,7 +225,7 @@ describe('packet diagrams', () => {
});
it('should throw error if numbers are not continuous for single packets with bit counts', async () => {
const str = `packet
const str = `packet-beta
+16: "test"
18: "error"
`;
@@ -241,7 +235,7 @@ describe('packet diagrams', () => {
});
it('should throw error if numbers are not continuous for single packets - 2', async () => {
const str = `packet
const str = `packet-beta
0-16: "test"
17: "good"
19: "error"
@@ -252,7 +246,7 @@ describe('packet diagrams', () => {
});
it('should throw error if end is less than start', async () => {
const str = `packet
const str = `packet-beta
0-16: "test"
25-20: "error"
`;
@@ -262,7 +256,7 @@ describe('packet diagrams', () => {
});
it('should throw error if bit count is 0', async () => {
const str = `packet
const str = `packet-beta
+0: "test"
`;
await expect(parser.parse(str)).rejects.toThrowErrorMatchingInlineSnapshot(

View File

@@ -524,7 +524,7 @@ export const drawBox = function (elem, box, conf) {
box.name,
g,
box.x,
box.y + conf.boxTextMargin + (box.textMaxHeight || 0) / 2,
box.y + (box.textMaxHeight || 0) / 2,
box.width,
0,
{ class: 'text' },

View File

@@ -28,7 +28,8 @@ interface TimelineTask {
export const draw = function (text: string, id: string, version: string, diagObj: Diagram) {
//1. Fetch the configuration
const conf = getConfig();
const LEFT_MARGIN = conf.timeline?.leftMargin ?? 50;
// @ts-expect-error - wrong config?
const LEFT_MARGIN = conf.leftMargin ?? 50;
log.debug('timeline', diagObj.db);

View File

@@ -302,7 +302,7 @@ If you are adding a feature, you will definitely need to add tests. Depending on
Unit tests are tests that test a single function or module. They are the easiest to write and the fastest to run.
Unit tests are mandatory for all code except the layout tests. (The layouts are tested with integration tests.)
Unit tests are mandatory for all code except the renderers. (The renderers are tested with integration tests.)
We use [Vitest](https://vitest.dev) to run unit tests.
@@ -328,30 +328,6 @@ When using Docker prepend your command with `./run`:
./run pnpm test
```
##### Testing the DOM
One can use `jsdomIt` to test any part of Mermaid that interacts with the DOM, as long as it is not related to the layout.
The function `jsdomIt` ([developed in utils.ts](../../tests/util.ts)) overrides `it` from `vitest`, and creates a pseudo-browser environment that works almost like the real deal for the duration of the test. It uses JSDOM to create a DOM, and adds objects `window` and `document` to `global` to mock the browser environment.
> [!NOTE]
> The layout cannot work in `jsdomIt` tests because JSDOM has no rendering engine, hence the pseudo-browser environment.
Example :
```typescript
import { ensureNodeFromSelector, jsdomIt } from './tests/util.js';
jsdomIt('should add element "thing" in the SVG', ({ svg }) => {
// Code in this block runs in a pseudo-browser environment
addThing(svg); // The svg item is the D3 selection of the SVG node
const svgNode = ensureNodeFromSelector('svg'); // Retrieve the DOM node using the DOM API
expect(svgNode.querySelector('thing')).not.toBeNull(); // Test the structure of the SVG
});
```
They can be used to test any method that interacts with the DOM, including for testing renderers. For renderers, additional integration testing is necessary to test the layout though.
#### Integration / End-to-End (E2E) Tests
These test the rendering and visual appearance of the diagrams.

View File

@@ -27,7 +27,7 @@
"@iconify-json/carbon": "^1.1.37",
"@unocss/reset": "^66.0.0",
"@vite-pwa/vitepress": "^1.0.0",
"@vitejs/plugin-vue": "^6.0.0",
"@vitejs/plugin-vue": "^5.0.5",
"fast-glob": "^3.3.3",
"https-localhost": "^4.7.1",
"pathe": "^2.0.3",

View File

@@ -11,7 +11,7 @@ This diagram type is particularly useful for developers, network engineers, educ
## Syntax
```
packet
packet-beta
start: "Block name" %% Single-bit block
start-end: "Block name" %% Multi-bit blocks
... More Fields ...
@@ -22,7 +22,7 @@ start-end: "Block name" %% Multi-bit blocks
Using start and end bit counts can be difficult, especially when modifying a design. For this we add a bit count field, which starts from the end of the previous field automagically. Use `+<count>` to set the number of bits, thus:
```
packet
packet-beta
+1: "Block name" %% Single-bit block
+8: "Block name" %% 8-bit block
9-15: "Manually set start and end, it's fine to mix and match"
@@ -35,7 +35,7 @@ packet
---
title: "TCP Packet"
---
packet
packet-beta
0-15: "Source Port"
16-31: "Destination Port"
32-63: "Sequence Number"
@@ -56,7 +56,7 @@ packet
```
```mermaid-example
packet
packet-beta
title UDP Packet
+16: "Source Port"
+16: "Destination Port"
@@ -104,7 +104,7 @@ config:
packet:
startByteColor: red
---
packet
packet-beta
0-15: "Source Port"
16-31: "Destination Port"
32-63: "Sequence Number"

View File

@@ -1,5 +1,40 @@
import { assert, beforeEach, describe, expect, it, vi } from 'vitest';
// -------------------------------------
// Mocks and mocking
import { MockedD3 } from './tests/MockedD3.js';
// Note: If running this directly from within an IDE, the mocks directory must be at packages/mermaid/mocks
vi.mock('d3');
vi.mock('dagre-d3');
// mermaidAPI.spec.ts:
import * as accessibility from './accessibility.js'; // Import it this way so we can use spyOn(accessibility,...)
vi.mock('./accessibility.js', () => ({
setA11yDiagramInfo: vi.fn(),
addSVGa11yTitleDescription: vi.fn(),
}));
// Mock the renderers specifically so we can test render(). Need to mock draw() for each renderer
vi.mock('./diagrams/c4/c4Renderer.js');
vi.mock('./diagrams/class/classRenderer.js');
vi.mock('./diagrams/class/classRenderer-v2.js');
vi.mock('./diagrams/er/erRenderer.js');
vi.mock('./diagrams/flowchart/flowRenderer-v2.js');
vi.mock('./diagrams/git/gitGraphRenderer.js');
vi.mock('./diagrams/gantt/ganttRenderer.js');
vi.mock('./diagrams/user-journey/journeyRenderer.js');
vi.mock('./diagrams/pie/pieRenderer.js');
vi.mock('./diagrams/packet/renderer.js');
vi.mock('./diagrams/xychart/xychartRenderer.js');
vi.mock('./diagrams/requirement/requirementRenderer.js');
vi.mock('./diagrams/sequence/sequenceRenderer.js');
vi.mock('./diagrams/radar/renderer.js');
vi.mock('./diagrams/architecture/architectureRenderer.js');
// -------------------------------------
import assignWithDepth from './assignWithDepth.js';
import type { MermaidConfig } from './config.type.js';
import mermaid from './mermaid.js';
@@ -40,9 +75,6 @@ import { SequenceDB } from './diagrams/sequence/sequenceDb.js';
import { decodeEntities, encodeEntities } from './utils.js';
import { toBase64 } from './utils/base64.js';
import { StateDB } from './diagrams/state/stateDb.js';
import { ensureNodeFromSelector, jsdomIt } from './tests/util.js';
import { select } from 'd3';
import { JSDOM } from 'jsdom';
/**
* @see https://vitest.dev/guide/mocking.html Mock part of a module
@@ -193,49 +225,63 @@ describe('mermaidAPI', () => {
});
});
const fauxParentNode = new MockedD3();
const fauxEnclosingDiv = new MockedD3();
const fauxSvgNode = new MockedD3();
describe('appendDivSvgG', () => {
const fauxGNode = new MockedD3();
const parent_append_spy = vi.spyOn(fauxParentNode, 'append').mockReturnValue(fauxEnclosingDiv);
const div_append_spy = vi.spyOn(fauxEnclosingDiv, 'append').mockReturnValue(fauxSvgNode);
// @ts-ignore @todo TODO why is this getting a type error?
const div_attr_spy = vi.spyOn(fauxEnclosingDiv, 'attr').mockReturnValue(fauxEnclosingDiv);
const svg_append_spy = vi.spyOn(fauxSvgNode, 'append').mockReturnValue(fauxGNode);
// @ts-ignore @todo TODO why is this getting a type error?
const svg_attr_spy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
// cspell:ignore dthe
jsdomIt('appends a div node', ({ body }) => {
appendDivSvgG(body, 'theId', 'dtheId');
const divNode = ensureNodeFromSelector('div');
const svgNode = ensureNodeFromSelector('svg', divNode);
ensureNodeFromSelector('g', svgNode);
it('appends a div node', () => {
appendDivSvgG(fauxParentNode, 'theId', 'dtheId');
expect(parent_append_spy).toHaveBeenCalledWith('div');
expect(div_append_spy).toHaveBeenCalledWith('svg');
});
jsdomIt('the id for the div is "d" with the id appended', ({ body }) => {
appendDivSvgG(body, 'theId', 'dtheId');
const divNode = ensureNodeFromSelector('div');
expect(divNode?.getAttribute('id')).toBe('dtheId');
it('the id for the div is "d" with the id appended', () => {
appendDivSvgG(fauxParentNode, 'theId', 'dtheId');
expect(div_attr_spy).toHaveBeenCalledWith('id', 'dtheId');
});
jsdomIt('sets the style for the div if one is given', ({ body }) => {
appendDivSvgG(body, 'theId', 'dtheId', 'given div style', 'given x link');
const divNode = ensureNodeFromSelector('div');
expect(divNode?.getAttribute('style')).toBe('given div style');
it('sets the style for the div if one is given', () => {
appendDivSvgG(fauxParentNode, 'theId', 'dtheId', 'given div style', 'given x link');
expect(div_attr_spy).toHaveBeenCalledWith('style', 'given div style');
});
jsdomIt('sets the svg width to 100%', ({ body }) => {
appendDivSvgG(body, 'theId', 'dtheId');
const svgNode = ensureNodeFromSelector('div > svg');
expect(svgNode.getAttribute('width')).toBe('100%');
it('appends a svg node to the div node', () => {
appendDivSvgG(fauxParentNode, 'theId', 'dtheId');
expect(div_attr_spy).toHaveBeenCalledWith('id', 'dtheId');
});
jsdomIt('the svg id is the id', ({ body }) => {
appendDivSvgG(body, 'theId', 'dtheId');
const svgNode = ensureNodeFromSelector('div > svg');
expect(svgNode.getAttribute('id')).toBe('theId');
it('sets the svg width to 100%', () => {
appendDivSvgG(fauxParentNode, 'theId', 'dtheId');
expect(svg_attr_spy).toHaveBeenCalledWith('width', '100%');
});
jsdomIt('the svg xml namespace is the 2000 standard', ({ body }) => {
appendDivSvgG(body, 'theId', 'dtheId');
const svgNode = ensureNodeFromSelector('div > svg');
expect(svgNode.getAttribute('xmlns')).toBe('http://www.w3.org/2000/svg');
it('the svg id is the id', () => {
appendDivSvgG(fauxParentNode, 'theId', 'dtheId');
expect(svg_attr_spy).toHaveBeenCalledWith('id', 'theId');
});
jsdomIt('sets the svg xlink if one is given', ({ body }) => {
appendDivSvgG(body, 'theId', 'dtheId', 'div style', 'given x link');
const svgNode = ensureNodeFromSelector('div > svg');
expect(svgNode.getAttribute('xmlns:xlink')).toBe('given x link');
it('the svg xml namespace is the 2000 standard', () => {
appendDivSvgG(fauxParentNode, 'theId', 'dtheId');
expect(svg_attr_spy).toHaveBeenCalledWith('xmlns', 'http://www.w3.org/2000/svg');
});
jsdomIt('returns the given parentRoot d3 nodes', ({ body }) => {
expect(appendDivSvgG(body, 'theId', 'dtheId')).toEqual(body);
it('sets the svg xlink if one is given', () => {
appendDivSvgG(fauxParentNode, 'theId', 'dtheId', 'div style', 'given x link');
expect(svg_attr_spy).toHaveBeenCalledWith('xmlns:xlink', 'given x link');
});
it('appends a g (group) node to the svg node', () => {
appendDivSvgG(fauxParentNode, 'theId', 'dtheId');
expect(svg_append_spy).toHaveBeenCalledWith('g');
});
it('returns the given parentRoot d3 nodes', () => {
expect(appendDivSvgG(fauxParentNode, 'theId', 'dtheId')).toEqual(fauxParentNode);
});
});
@@ -736,9 +782,9 @@ graph TD;A--x|text including URL space|B;`)
// render(id, text, cb?, svgContainingElement?)
// Test all diagram types. Note that old flowchart 'graph' type will invoke the flowRenderer-v2. (See the flowchart v2 detector.)
// We have to have both the specific textDiagramType and the expected type name because the expected type may be slightly different from what is put in the diagram text (ex: in -v2 diagrams)
// We have to have both the specific textDiagramType and the expected type name because the expected type may be slightly different than was is put in the diagram text (ex: in -v2 diagrams)
const diagramTypesAndExpectations = [
// { textDiagramType: 'C4Context', expectedType: 'c4' }, TODO : setAccTitle not called in C4 jison parser
{ textDiagramType: 'C4Context', expectedType: 'c4' },
{ textDiagramType: 'classDiagram', expectedType: 'class' },
{ textDiagramType: 'classDiagram-v2', expectedType: 'classDiagram' },
{ textDiagramType: 'erDiagram', expectedType: 'er' },
@@ -748,13 +794,8 @@ graph TD;A--x|text including URL space|B;`)
{ textDiagramType: 'gantt', expectedType: 'gantt' },
{ textDiagramType: 'journey', expectedType: 'journey' },
{ textDiagramType: 'pie', expectedType: 'pie' },
{ textDiagramType: 'packet', expectedType: 'packet' },
{ textDiagramType: 'packet-beta', expectedType: 'packet' },
{
textDiagramType: 'xychart-beta',
expectedType: 'xychart',
content: 'x-axis "Attempts" 10000 --> 10000\ny-axis "Passing tests" 1 --> 1\nbar [1]',
},
{ textDiagramType: 'xychart-beta', expectedType: 'xychart' },
{ textDiagramType: 'requirementDiagram', expectedType: 'requirement' },
{ textDiagramType: 'sequenceDiagram', expectedType: 'sequence' },
{ textDiagramType: 'stateDiagram-v2', expectedType: 'stateDiagram' },
@@ -770,25 +811,20 @@ graph TD;A--x|text including URL space|B;`)
diagramTypesAndExpectations.forEach((testedDiagram) => {
describe(`${testedDiagram.textDiagramType}`, () => {
const diagramType = testedDiagram.textDiagramType;
const content = testedDiagram.content || '';
const diagramText = `${diagramType}\n accTitle: ${a11yTitle}\n accDescr: ${a11yDescr}\n ${content}`;
const diagramText = `${diagramType}\n accTitle: ${a11yTitle}\n accDescr: ${a11yDescr}\n`;
const expectedDiagramType = testedDiagram.expectedType;
jsdomIt(
'should set aria-roledescription to the diagram type AND should call addSVGa11yTitleDescription',
async () => {
const { svg } = await mermaidAPI.render(id, diagramText);
const dom = new JSDOM(svg);
const svgNode = ensureNodeFromSelector('svg', dom.window.document);
const descNode = ensureNodeFromSelector('desc', svgNode);
const titleNode = ensureNodeFromSelector('title', svgNode);
expect(svgNode.getAttribute('aria-roledescription')).toBe(expectedDiagramType);
expect(svgNode.getAttribute('aria-describedby')).toBe(`chart-desc-${id}`);
expect(descNode.getAttribute('id')).toBe(`chart-desc-${id}`);
expect(descNode.innerHTML).toBe(a11yDescr);
expect(titleNode.innerHTML).toBe(a11yTitle);
}
);
it('should set aria-roledescription to the diagram type AND should call addSVGa11yTitleDescription', async () => {
const a11yDiagramInfo_spy = vi.spyOn(accessibility, 'setA11yDiagramInfo');
const a11yTitleDesc_spy = vi.spyOn(accessibility, 'addSVGa11yTitleDescription');
const result = await mermaidAPI.render(id, diagramText);
expect(result.diagramType).toBe(expectedDiagramType);
expect(a11yDiagramInfo_spy).toHaveBeenCalledWith(
expect.anything(),
expectedDiagramType
);
expect(a11yTitleDesc_spy).toHaveBeenCalled();
});
});
});
});

View File

@@ -0,0 +1,150 @@
/**
* This is a mocked/stubbed version of the d3 Selection type. Each of the main functions are all
* mocked (via vi.fn()) so you can track if they have been called, etc.
*
* Note that node() returns a HTML Element with tag 'svg'. It is an empty element (no innerHTML, no children, etc).
* This potentially allows testing of mermaidAPI render().
*/
export class MockedD3 {
public attribs = new Map<string, string>();
public id: string | undefined = '';
_children: MockedD3[] = [];
_containingHTMLdoc = new Document();
constructor(givenId = 'mock-id') {
this.id = givenId;
}
/** Helpful utility during development/debugging. This is not a real d3 function */
public listChildren(): string {
return this._children
.map((child) => {
return child.id;
})
.join(', ');
}
select = vi.fn().mockImplementation(({ select_str = '' }): MockedD3 => {
// Get the id from an argument string. if it is of the form [id='some-id'], strip off the
// surrounding id[..]
const stripSurroundRegexp = /\[id='(.*)']/;
const matchedSurrounds = select_str.match(stripSurroundRegexp);
const cleanId = matchedSurrounds ? matchedSurrounds[1] : select_str;
return new MockedD3(cleanId);
});
// This has the same implementation as select(). (It calls it.)
selectAll = vi.fn().mockImplementation(({ select_str = '' }): MockedD3 => {
return this.select(select_str);
});
append = vi.fn().mockImplementation(function (
this: MockedD3,
type: string,
id = '' + '-appended'
): MockedD3 {
const newMock = new MockedD3(id);
newMock.attribs.set('type', type);
this._children.push(newMock);
return newMock;
});
// NOTE: The d3 implementation allows for a selector ('beforeSelector' arg below).
// With this mocked implementation, we assume it will always refer to a node id
// and will always be of the form "#[id of the node to insert before]".
// To keep this simple, any leading '#' is removed and the resulting string is the node id searched.
insert = (type: string, beforeSelector?: string, id = this.id + '-inserted'): MockedD3 => {
const newMock = new MockedD3(id);
newMock.attribs.set('type', type);
if (beforeSelector === undefined) {
this._children.push(newMock);
} else {
const idOnly = beforeSelector.startsWith('#') ? beforeSelector.substring(1) : beforeSelector;
const foundIndex = this._children.findIndex((child) => child.id === idOnly);
if (foundIndex < 0) {
this._children.push(newMock);
} else {
this._children.splice(foundIndex, 0, newMock);
}
}
return newMock;
};
attr(attrName: string): undefined | string;
attr(attrName: string, attrValue: string): MockedD3;
attr(attrName: string, attrValue?: string): undefined | string | MockedD3 {
if (arguments.length === 1) {
return this.attribs.get(attrName);
} else {
if (attrName === 'id') {
this.id = attrValue; // also set the id explicitly
}
if (attrValue !== undefined) {
this.attribs.set(attrName, attrValue);
}
return this;
}
}
public lower(attrValue = '') {
this.attribs.set('lower', attrValue);
return this;
}
public style(attrValue = '') {
this.attribs.set('style', attrValue);
return this;
}
public text(attrValue = '') {
this.attribs.set('text', attrValue);
return this;
}
// NOTE: Returns a HTML Element with tag 'svg' that has _another_ 'svg' element child.
// This allows different tests to succeed -- some need a top level 'svg' and some need a 'svg' element to be the firstChild
// Real implementation returns an HTML Element
public node = vi.fn().mockImplementation(() => {
//create a top level svg element
const topElem = this._containingHTMLdoc.createElement('svg');
//@ts-ignore - this is a mock SVG element
topElem.getBBox = this.getBBox;
const elem_svgChild = this._containingHTMLdoc.createElement('svg'); // another svg element
topElem.appendChild(elem_svgChild);
return topElem;
});
// TODO Is this correct? shouldn't it return a list of HTML Elements?
nodes = vi.fn().mockImplementation(function (this: MockedD3): MockedD3[] {
return this._children;
});
// This will try to use attrs that have been set.
getBBox = () => {
const x = this.attribs.has('x') ? this.attribs.get('x') : 20;
const y = this.attribs.has('y') ? this.attribs.get('y') : 30;
const width = this.attribs.has('width') ? this.attribs.get('width') : 140;
const height = this.attribs.has('height') ? this.attribs.get('height') : 250;
return {
x: x,
y: y,
width: width,
height: height,
};
};
// --------------------------------------------------------------------------------
// The following functions are here for completeness. They simply return a vi.fn()
insertBefore = vi.fn();
curveBasis = vi.fn();
curveBasisClosed = vi.fn();
curveBasisOpen = vi.fn();
curveLinear = vi.fn();
curveLinearClosed = vi.fn();
curveMonotoneX = vi.fn();
curveMonotoneY = vi.fn();
curveNatural = vi.fn();
curveStep = vi.fn();
curveStepAfter = vi.fn();
curveStepBefore = vi.fn();
}

View File

@@ -0,0 +1,3 @@
import { vi } from 'vitest';
vi.mock('d3');
vi.mock('dagre-d3-es');

View File

@@ -26,10 +26,6 @@ ${'2w'} | ${dayjs.duration(2, 'w')}
```
*/
import { JSDOM } from 'jsdom';
import { expect, it } from 'vitest';
import { select, type Selection } from 'd3';
export const convert = (template: TemplateStringsArray, ...params: unknown[]) => {
const header = template[0]
.trim()
@@ -46,83 +42,3 @@ export const convert = (template: TemplateStringsArray, ...params: unknown[]) =>
}
return out;
};
/**
* Getting rid of linter issues to make {@link jsdomIt} work.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function setOnProtectedConstant(object: any, key: string, value: unknown): void {
object[key] = value;
}
export const MOCKED_BBOX = {
x: 0,
y: 0,
width: 666,
height: 666,
};
interface JsdomItInput {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
body: Selection<HTMLBodyElement, never, HTMLElement, any>; // The `any` here comes from D3'as API.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
svg: Selection<SVGSVGElement, never, HTMLElement, any>; // The `any` here comes from D3'as API.
}
/**
* Test method borrowed from d3 : https://github.com/d3/d3-selection/blob/v3.0.0/test/jsdom.js
*
* Fools d3 into thinking it's working in a browser with a real DOM.
*
* The DOM is actually an instance of JSDom with monkey-patches for DOM methods that require a
* rendering engine.
*
* The resulting environment is capable of rendering SVGs with the caveat that layouts are
* completely screwed.
*
* This makes it possible to make structural tests instead of mocking everything.
*/
export function jsdomIt(message: string, run: (input: JsdomItInput) => void | Promise<void>) {
return it(message, async (): Promise<void> => {
const oldWindow = global.window;
const oldDocument = global.document;
try {
const baseHtml = `
<html lang="en">
<body id="cy">
<svg id="svg"/>
</body>
</html>
`;
const dom = new JSDOM(baseHtml, {
resources: 'usable',
beforeParse(_window) {
// Mocks DOM functions that require rendering, JSDOM doesn't
setOnProtectedConstant(_window.Element.prototype, 'getBBox', () => MOCKED_BBOX);
setOnProtectedConstant(_window.Element.prototype, 'getComputedTextLength', () => 200);
},
});
setOnProtectedConstant(global, 'window', dom.window); // Fool D3 into thinking it's in a browser
setOnProtectedConstant(global, 'document', dom.window.document); // Fool D3 into thinking it's in a browser
setOnProtectedConstant(global, 'MutationObserver', undefined); // JSDOM doesn't like cytoscape elements
const body = select<HTMLBodyElement, never>('body');
const svg = select<SVGSVGElement, never>('svg');
await run({ body, svg });
} finally {
setOnProtectedConstant(global, 'window', oldWindow);
setOnProtectedConstant(global, 'document', oldDocument);
}
});
}
/**
* Retrieves the node from its parent with ParentNode#querySelector,
* then checks that it exists before returning it.
*/
export function ensureNodeFromSelector(selector: string, parent: ParentNode = document): Element {
const node = parent.querySelector(selector);
expect(node).not.toBeNull();
return node!;
}

View File

@@ -1,11 +1,11 @@
import { expect, vi } from 'vitest';
import { vi } from 'vitest';
import utils, { calculatePoint, cleanAndMerge, detectDirective } from './utils.js';
import assignWithDepth from './assignWithDepth.js';
import { detectType } from './diagram-api/detectType.js';
import { addDiagrams } from './diagram-api/diagram-orchestration.js';
import memoize from 'lodash-es/memoize.js';
import { MockedD3 } from './tests/MockedD3.js';
import { preprocessDiagram } from './preprocess.js';
import { MOCKED_BBOX, ensureNodeFromSelector, jsdomIt } from './tests/util.js';
addDiagrams();
@@ -369,34 +369,53 @@ describe('when initializing the id generator', function () {
});
describe('when inserting titles', function () {
jsdomIt('does nothing if the title is empty', function ({ svg }) {
const svg = new MockedD3('svg');
const mockedElement = {
getBBox: vi.fn().mockReturnValue({ x: 10, y: 11, width: 100, height: 200 }),
};
const fauxTitle = new MockedD3('title');
beforeEach(() => {
svg.node = vi.fn().mockReturnValue(mockedElement);
});
it('does nothing if the title is empty', function () {
const svgAppendSpy = vi.spyOn(svg, 'append');
utils.insertTitle(svg, 'testClass', 0, '');
const titleNode = document.querySelector('svg > text');
expect(titleNode).toBeNull();
expect(svgAppendSpy).not.toHaveBeenCalled();
});
jsdomIt('appends the title as a text item with the given title text', function ({ svg }) {
it('appends the title as a text item with the given title text', function () {
const svgAppendSpy = vi.spyOn(svg, 'append').mockReturnValue(fauxTitle);
const titleTextSpy = vi.spyOn(fauxTitle, 'text');
utils.insertTitle(svg, 'testClass', 5, 'test title');
const titleNode = ensureNodeFromSelector('svg > text');
expect(titleNode.innerHTML).toBe('test title');
expect(svgAppendSpy).toHaveBeenCalled();
expect(titleTextSpy).toHaveBeenCalledWith('test title');
});
jsdomIt('x value is the bounds x position + half of the bounds width', ({ svg }) => {
it('x value is the bounds x position + half of the bounds width', () => {
vi.spyOn(svg, 'append').mockReturnValue(fauxTitle);
const titleAttrSpy = vi.spyOn(fauxTitle, 'attr');
utils.insertTitle(svg, 'testClass', 5, 'test title');
const titleNode = ensureNodeFromSelector('svg > text');
expect(titleNode.getAttribute('x')).toBe(`${MOCKED_BBOX.x + MOCKED_BBOX.width / 2}`);
expect(titleAttrSpy).toHaveBeenCalledWith('x', 10 + 100 / 2);
});
jsdomIt('y value is the negative of given title top margin', ({ svg }) => {
it('y value is the negative of given title top margin', () => {
vi.spyOn(svg, 'append').mockReturnValue(fauxTitle);
const titleAttrSpy = vi.spyOn(fauxTitle, 'attr');
utils.insertTitle(svg, 'testClass', 5, 'test title');
const titleNode = ensureNodeFromSelector('svg > text');
expect(titleNode.getAttribute('y')).toBe(`${MOCKED_BBOX.y - 5}`);
expect(titleAttrSpy).toHaveBeenCalledWith('y', -5);
});
jsdomIt('class is the given css class', ({ svg }) => {
it('class is the given css class', () => {
vi.spyOn(svg, 'append').mockReturnValue(fauxTitle);
const titleAttrSpy = vi.spyOn(fauxTitle, 'attr');
utils.insertTitle(svg, 'testClass', 5, 'test title');
const titleNode = ensureNodeFromSelector('svg > text');
expect(titleNode.getAttribute('class')).toBe('testClass');
expect(titleAttrSpy).toHaveBeenCalledWith('class', 'testClass');
});
});

View File

@@ -3,7 +3,7 @@ import "../common/common";
entry Packet:
NEWLINE*
("packet"| "packet-beta")
"packet-beta"
(
TitleAndAccessibilities
| blocks+=PacketBlock

View File

@@ -2,6 +2,6 @@ import { AbstractMermaidTokenBuilder } from '../common/index.js';
export class PacketTokenBuilder extends AbstractMermaidTokenBuilder {
public constructor() {
super(['packet']);
super(['packet-beta']);
}
}

View File

@@ -11,12 +11,6 @@ describe('packet', () => {
`
\tpacket-beta
`,
`packet`,
` packet `,
`\tpacket\t`,
`
\tpacket
`,
])('should handle regular packet', (context: string) => {
const result = parse(context);
expectNoErrorsOrAlternatives(result);

View File

@@ -32,10 +32,15 @@ const consoleMock = vi.spyOn(console, 'log').mockImplementation(() => undefined)
* @param result - the result `parse` function.
*/
export function expectNoErrorsOrAlternatives(result: ParseResult) {
expect.soft(result.lexerErrors).toHaveLength(0);
expect.soft(result.parserErrors).toHaveLength(0);
// To see what the error is, in the logs.
expect(result.lexerErrors[0]).toBeUndefined();
if (result.lexerErrors.length > 0) {
// console.debug(result.lexerErrors);
}
if (result.parserErrors.length > 0) {
// console.debug(result.parserErrors);
}
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
expect(consoleMock).not.toHaveBeenCalled();
consoleMock.mockReset();
}

3487
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -40,10 +40,6 @@
{
"groupName": "dompurify",
"matchPackagePatterns": ["dompurify"]
},
{
"matchPackageNames": ["chokidar"],
"enabled": false
}
],
"dependencyDashboard": false,

View File

@@ -16,6 +16,7 @@ export default defineConfig({
environment: 'jsdom',
globals: true,
// TODO: should we move this to a mermaid-core package?
setupFiles: ['packages/mermaid/src/tests/setup.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html', 'lcov'],