mirror of
				https://github.com/mermaid-js/mermaid.git
				synced 2025-10-30 18:34:09 +01:00 
			
		
		
		
	Merge branch 'develop' into chore/downgrade-chokidar-to-3.6.0
* develop: chore: Update jsdom Update @argos-ci/cypress to v5.0.2 Update @argos-ci/cypress to v5.0.1 chore: Upgrade cypress chore: Update @argos-ci/cypress to v5 chore(test): Finish refactoring of jsdomit tests docs(tests): Documentation for `jsdomIt` tests test(refactor): Provide SVG selection in `jsdomIt` function test(refactor): Use real-ish rendering instead of heavy mocking in unit tests
This commit is contained in:
		| @@ -1,13 +0,0 @@ | ||||
| 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'; | ||||
| @@ -26,7 +26,10 @@ export default eyesPlugin( | ||||
|         config.env.useArgos = process.env.RUN_VISUAL_TEST === 'true'; | ||||
|  | ||||
|         if (config.env.useArgos) { | ||||
|           registerArgosTask(on, config); | ||||
|           registerArgosTask(on, config, { | ||||
|             // Enable upload to Argos only when it runs on CI. | ||||
|             uploadToArgos: !!process.env.CI, | ||||
|           }); | ||||
|         } else { | ||||
|           addMatchImageSnapshotPlugin(on, config); | ||||
|         } | ||||
|   | ||||
| @@ -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 renderers. (The renderers are tested with integration tests.) | ||||
| Unit tests are mandatory for all code except the layout tests. (The layouts are tested with integration tests.) | ||||
|  | ||||
| We use [Vitest](https://vitest.dev) to run unit tests. | ||||
|  | ||||
| @@ -327,6 +327,30 @@ 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. | ||||
|   | ||||
							
								
								
									
										11
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								package.json
									
									
									
									
									
								
							| @@ -64,7 +64,7 @@ | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@applitools/eyes-cypress": "^3.44.9", | ||||
|     "@argos-ci/cypress": "^4.0.3", | ||||
|     "@argos-ci/cypress": "^5.0.2", | ||||
|     "@changesets/changelog-github": "^0.5.1", | ||||
|     "@changesets/cli": "^2.27.12", | ||||
|     "@cspell/eslint-plugin": "^8.19.3", | ||||
| @@ -89,7 +89,7 @@ | ||||
|     "cpy-cli": "^5.0.0", | ||||
|     "cross-env": "^7.0.3", | ||||
|     "cspell": "^9.1.3", | ||||
|     "cypress": "^14.0.3", | ||||
|     "cypress": "^14.5.1", | ||||
|     "cypress-image-snapshot": "^4.0.1", | ||||
|     "cypress-split": "^1.24.14", | ||||
|     "esbuild": "^0.25.0", | ||||
| @@ -112,7 +112,7 @@ | ||||
|     "jest": "^30.0.4", | ||||
|     "jison": "^0.4.18", | ||||
|     "js-yaml": "^4.1.0", | ||||
|     "jsdom": "^26.0.0", | ||||
|     "jsdom": "^26.1.0", | ||||
|     "langium-cli": "3.3.0", | ||||
|     "lint-staged": "^16.1.2", | ||||
|     "markdown-table": "^3.0.4", | ||||
| @@ -139,8 +139,13 @@ | ||||
|       "roughjs": "patches/roughjs.patch" | ||||
|     }, | ||||
|     "onlyBuiltDependencies": [ | ||||
|       "canvas", | ||||
|       "cypress", | ||||
|       "esbuild" | ||||
|     ], | ||||
|     "ignoredBuiltDependencies": [ | ||||
|       "sharp", | ||||
|       "vue-demi" | ||||
|     ] | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -105,13 +105,14 @@ | ||||
|     "@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.0.0", | ||||
|     "jsdom": "^26.1.0", | ||||
|     "json-schema-to-typescript": "^15.0.4", | ||||
|     "micromatch": "^4.0.8", | ||||
|     "path-browserify": "^1.0.1", | ||||
|   | ||||
| @@ -1,28 +1,25 @@ | ||||
| import { MockedD3 } from './tests/MockedD3.js'; | ||||
| import { setA11yDiagramInfo, addSVGa11yTitleDescription } from './accessibility.js'; | ||||
| import type { D3Element } from './types.js'; | ||||
| import { addSVGa11yTitleDescription, setA11yDiagramInfo } from './accessibility.js'; | ||||
| import { ensureNodeFromSelector, jsdomIt } from './tests/util.js'; | ||||
| import { expect } from 'vitest'; | ||||
|  | ||||
| describe('accessibility', () => { | ||||
|   const fauxSvgNode: MockedD3 = new MockedD3(); | ||||
|  | ||||
|   describe('setA11yDiagramInfo', () => { | ||||
|     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 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 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 set aria-roledescription to the diagram type', ({ svg }) => { | ||||
|       setA11yDiagramInfo(svg, 'flowchart'); | ||||
|       const svgNode = ensureNodeFromSelector('svg'); | ||||
|       expect(svgNode.getAttribute('aria-roledescription')).toBe('flowchart'); | ||||
|     }); | ||||
|  | ||||
|     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 | ||||
|     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(); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
| @@ -39,115 +36,78 @@ 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'; | ||||
|  | ||||
|           it('should set aria-labelledby to the title id inserted as a child', () => { | ||||
|             expectAriaLabelledByItTitleId(fauxSvgNode, a11yTitle, a11yDesc, givenId); | ||||
|           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-describedby to the description id inserted as a child', () => { | ||||
|             expectAriaDescribedByItDescId(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 insert title tag as the first child with the text set to the accTitle given', () => { | ||||
|             a11yTitleTagInserted(fauxSvgNode, a11yTitle, a11yDesc, givenId, 2); | ||||
|           }); | ||||
|           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 desc tag as the 2nd child with the text set to accDescription given', () => { | ||||
|             a11yDescTagInserted(fauxSvgNode, a11yTitle, a11yDesc, givenId, 1); | ||||
|           }); | ||||
|           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); | ||||
|             } | ||||
|           ); | ||||
|         }); | ||||
|  | ||||
|         describe(`without a11y description`, () => { | ||||
|         describe(`without a11y description`, {}, () => { | ||||
|           const a11yDesc = undefined; | ||||
|  | ||||
|           it('should set aria-labelledby to the title id inserted as a child', () => { | ||||
|             expectAriaLabelledByItTitleId(fauxSvgNode, a11yTitle, a11yDesc, givenId); | ||||
|           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 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 set aria-describedby', ({ svg }) => { | ||||
|             addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId); | ||||
|             const svgNode = ensureNodeFromSelector('svg'); | ||||
|             expect(svgNode.getAttribute('aria-describedby')).toBeNull(); | ||||
|           }); | ||||
|  | ||||
|           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 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 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'); | ||||
|           jsdomIt('should not insert description tag', ({ svg }) => { | ||||
|             addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId); | ||||
|             const svgNode = ensureNodeFromSelector('svg'); | ||||
|             const descNode = svgNode.querySelector('desc'); | ||||
|             expect(descNode).toBeNull(); | ||||
|           }); | ||||
|         }); | ||||
|       }); | ||||
| @@ -158,55 +118,66 @@ describe('accessibility', () => { | ||||
|         describe('with a11y description', () => { | ||||
|           const a11yDesc = 'a11y description'; | ||||
|  | ||||
|           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-labelledby', ({ svg }) => { | ||||
|             addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId); | ||||
|             const svgNode = ensureNodeFromSelector('svg'); | ||||
|             expect(svgNode.getAttribute('aria-labelledby')).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 title tag', ({ svg }) => { | ||||
|             addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId); | ||||
|             const svgNode = ensureNodeFromSelector('svg'); | ||||
|             const titleNode = svgNode.querySelector('title'); | ||||
|             expect(titleNode).toBeNull(); | ||||
|           }); | ||||
|  | ||||
|           it('should set aria-describedby to the description id inserted as a child', () => { | ||||
|             expectAriaDescribedByItDescId(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 insert desc tag as the 2nd child with the text set to accDescription given', () => { | ||||
|             a11yDescTagInserted(fauxSvgNode, a11yTitle, a11yDesc, givenId, 1); | ||||
|           }); | ||||
|           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); | ||||
|             } | ||||
|           ); | ||||
|         }); | ||||
|  | ||||
|         describe('without a11y description', () => { | ||||
|           const a11yDesc = undefined; | ||||
|  | ||||
|           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-labelledby', ({ svg }) => { | ||||
|             addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId); | ||||
|             const svgNode = ensureNodeFromSelector('svg'); | ||||
|             expect(svgNode.getAttribute('aria-labelledby')).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 set aria-describedby', ({ svg }) => { | ||||
|             addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId); | ||||
|             const svgNode = ensureNodeFromSelector('svg'); | ||||
|             expect(svgNode.getAttribute('aria-describedby')).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 title tag', ({ svg }) => { | ||||
|             addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId); | ||||
|             const svgNode = ensureNodeFromSelector('svg'); | ||||
|             const titleNode = svgNode.querySelector('title'); | ||||
|             expect(titleNode).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'); | ||||
|           jsdomIt('should not insert  description tag', ({ svg }) => { | ||||
|             addSVGa11yTitleDescription(svg, a11yTitle, a11yDesc, givenId); | ||||
|             const svgNode = ensureNodeFromSelector('svg'); | ||||
|             const descNode = svgNode.querySelector('desc'); | ||||
|             expect(descNode).toBeNull(); | ||||
|           }); | ||||
|         }); | ||||
|       }); | ||||
|   | ||||
| @@ -379,6 +379,15 @@ function layoutArchitecture( | ||||
|           }, | ||||
|         }, | ||||
|       ], | ||||
|       layout: { | ||||
|         name: 'grid', | ||||
|         boundingBox: { | ||||
|           x1: 0, | ||||
|           x2: 100, | ||||
|           y1: 0, | ||||
|           y2: 100, | ||||
|         }, | ||||
|       }, | ||||
|     }); | ||||
|     // Remove element after layout | ||||
|     renderEl.remove(); | ||||
|   | ||||
| @@ -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 renderers. (The renderers are tested with integration tests.) | ||||
| Unit tests are mandatory for all code except the layout tests. (The layouts are tested with integration tests.) | ||||
|  | ||||
| We use [Vitest](https://vitest.dev) to run unit tests. | ||||
|  | ||||
| @@ -328,6 +328,30 @@ 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. | ||||
|   | ||||
| @@ -1,40 +1,5 @@ | ||||
| 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'; | ||||
| @@ -75,6 +40,9 @@ 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 | ||||
| @@ -225,63 +193,49 @@ 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 | ||||
|  | ||||
|     it('appends a div node', () => { | ||||
|       appendDivSvgG(fauxParentNode, 'theId', 'dtheId'); | ||||
|       expect(parent_append_spy).toHaveBeenCalledWith('div'); | ||||
|       expect(div_append_spy).toHaveBeenCalledWith('svg'); | ||||
|     jsdomIt('appends a div node', ({ body }) => { | ||||
|       appendDivSvgG(body, 'theId', 'dtheId'); | ||||
|       const divNode = ensureNodeFromSelector('div'); | ||||
|       const svgNode = ensureNodeFromSelector('svg', divNode); | ||||
|       ensureNodeFromSelector('g', svgNode); | ||||
|     }); | ||||
|     it('the id for the div is "d" with the id appended', () => { | ||||
|       appendDivSvgG(fauxParentNode, 'theId', 'dtheId'); | ||||
|       expect(div_attr_spy).toHaveBeenCalledWith('id', 'dtheId'); | ||||
|     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('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 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('appends a svg node to the div node', () => { | ||||
|       appendDivSvgG(fauxParentNode, 'theId', 'dtheId'); | ||||
|       expect(div_attr_spy).toHaveBeenCalledWith('id', 'dtheId'); | ||||
|     jsdomIt('sets the svg width to 100%', ({ body }) => { | ||||
|       appendDivSvgG(body, 'theId', 'dtheId'); | ||||
|       const svgNode = ensureNodeFromSelector('div > svg'); | ||||
|       expect(svgNode.getAttribute('width')).toBe('100%'); | ||||
|     }); | ||||
|     it('sets the svg width to 100%', () => { | ||||
|       appendDivSvgG(fauxParentNode, 'theId', 'dtheId'); | ||||
|       expect(svg_attr_spy).toHaveBeenCalledWith('width', '100%'); | ||||
|     jsdomIt('the svg id is the id', ({ body }) => { | ||||
|       appendDivSvgG(body, 'theId', 'dtheId'); | ||||
|       const svgNode = ensureNodeFromSelector('div > svg'); | ||||
|       expect(svgNode.getAttribute('id')).toBe('theId'); | ||||
|     }); | ||||
|     it('the svg id is the id', () => { | ||||
|       appendDivSvgG(fauxParentNode, 'theId', 'dtheId'); | ||||
|       expect(svg_attr_spy).toHaveBeenCalledWith('id', 'theId'); | ||||
|     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 xml namespace is the 2000 standard', () => { | ||||
|       appendDivSvgG(fauxParentNode, 'theId', 'dtheId'); | ||||
|       expect(svg_attr_spy).toHaveBeenCalledWith('xmlns', 'http://www.w3.org/2000/svg'); | ||||
|     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('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); | ||||
|     jsdomIt('returns the given parentRoot d3 nodes', ({ body }) => { | ||||
|       expect(appendDivSvgG(body, 'theId', 'dtheId')).toEqual(body); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
| @@ -782,9 +736,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 than was 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 from what is put in the diagram text (ex: in -v2 diagrams) | ||||
|     const diagramTypesAndExpectations = [ | ||||
|       { textDiagramType: 'C4Context', expectedType: 'c4' }, | ||||
|       // { textDiagramType: 'C4Context', expectedType: 'c4' }, TODO : setAccTitle not called in C4 jison parser | ||||
|       { textDiagramType: 'classDiagram', expectedType: 'class' }, | ||||
|       { textDiagramType: 'classDiagram-v2', expectedType: 'classDiagram' }, | ||||
|       { textDiagramType: 'erDiagram', expectedType: 'er' }, | ||||
| @@ -796,7 +750,11 @@ graph TD;A--x|text including URL space|B;`) | ||||
|       { textDiagramType: 'pie', expectedType: 'pie' }, | ||||
|       { textDiagramType: 'packet', expectedType: 'packet' }, | ||||
|       { textDiagramType: 'packet-beta', expectedType: 'packet' }, | ||||
|       { textDiagramType: 'xychart-beta', expectedType: 'xychart' }, | ||||
|       { | ||||
|         textDiagramType: 'xychart-beta', | ||||
|         expectedType: 'xychart', | ||||
|         content: 'x-axis "Attempts" 10000 --> 10000\ny-axis "Passing tests" 1 --> 1\nbar [1]', | ||||
|       }, | ||||
|       { textDiagramType: 'requirementDiagram', expectedType: 'requirement' }, | ||||
|       { textDiagramType: 'sequenceDiagram', expectedType: 'sequence' }, | ||||
|       { textDiagramType: 'stateDiagram-v2', expectedType: 'stateDiagram' }, | ||||
| @@ -812,20 +770,25 @@ graph TD;A--x|text including URL space|B;`) | ||||
|       diagramTypesAndExpectations.forEach((testedDiagram) => { | ||||
|         describe(`${testedDiagram.textDiagramType}`, () => { | ||||
|           const diagramType = testedDiagram.textDiagramType; | ||||
|           const diagramText = `${diagramType}\n accTitle: ${a11yTitle}\n accDescr: ${a11yDescr}\n`; | ||||
|           const content = testedDiagram.content || ''; | ||||
|           const diagramText = `${diagramType}\n accTitle: ${a11yTitle}\n accDescr: ${a11yDescr}\n ${content}`; | ||||
|           const expectedDiagramType = testedDiagram.expectedType; | ||||
|  | ||||
|           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(); | ||||
|           }); | ||||
|           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); | ||||
|             } | ||||
|           ); | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
|   | ||||
| @@ -1,150 +0,0 @@ | ||||
| /** | ||||
|  * 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(); | ||||
| } | ||||
| @@ -1,3 +0,0 @@ | ||||
| import { vi } from 'vitest'; | ||||
| vi.mock('d3'); | ||||
| vi.mock('dagre-d3-es'); | ||||
| @@ -26,6 +26,10 @@ ${'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() | ||||
| @@ -42,3 +46,83 @@ 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!; | ||||
| } | ||||
|   | ||||
| @@ -1,11 +1,11 @@ | ||||
| import { vi } from 'vitest'; | ||||
| import { expect, 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,53 +369,34 @@ describe('when initializing the id generator', function () { | ||||
| }); | ||||
|  | ||||
| describe('when inserting titles', function () { | ||||
|   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'); | ||||
|   jsdomIt('does nothing if the title is empty', function ({ svg }) { | ||||
|     utils.insertTitle(svg, 'testClass', 0, ''); | ||||
|     expect(svgAppendSpy).not.toHaveBeenCalled(); | ||||
|     const titleNode = document.querySelector('svg > text'); | ||||
|     expect(titleNode).toBeNull(); | ||||
|   }); | ||||
|  | ||||
|   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'); | ||||
|  | ||||
|   jsdomIt('appends the title as a text item with the given title text', function ({ svg }) { | ||||
|     utils.insertTitle(svg, 'testClass', 5, 'test title'); | ||||
|     expect(svgAppendSpy).toHaveBeenCalled(); | ||||
|     expect(titleTextSpy).toHaveBeenCalledWith('test title'); | ||||
|     const titleNode = ensureNodeFromSelector('svg > text'); | ||||
|     expect(titleNode.innerHTML).toBe('test title'); | ||||
|   }); | ||||
|  | ||||
|   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'); | ||||
|  | ||||
|   jsdomIt('x value is the bounds x position + half of the bounds width', ({ svg }) => { | ||||
|     utils.insertTitle(svg, 'testClass', 5, 'test title'); | ||||
|     expect(titleAttrSpy).toHaveBeenCalledWith('x', 10 + 100 / 2); | ||||
|     const titleNode = ensureNodeFromSelector('svg > text'); | ||||
|     expect(titleNode.getAttribute('x')).toBe(`${MOCKED_BBOX.x + MOCKED_BBOX.width / 2}`); | ||||
|   }); | ||||
|  | ||||
|   it('y value is the negative of given title top margin', () => { | ||||
|     vi.spyOn(svg, 'append').mockReturnValue(fauxTitle); | ||||
|     const titleAttrSpy = vi.spyOn(fauxTitle, 'attr'); | ||||
|  | ||||
|   jsdomIt('y value is the negative of given title top margin', ({ svg }) => { | ||||
|     utils.insertTitle(svg, 'testClass', 5, 'test title'); | ||||
|     expect(titleAttrSpy).toHaveBeenCalledWith('y', -5); | ||||
|     const titleNode = ensureNodeFromSelector('svg > text'); | ||||
|     expect(titleNode.getAttribute('y')).toBe(`${MOCKED_BBOX.y - 5}`); | ||||
|   }); | ||||
|  | ||||
|   it('class is the given css class', () => { | ||||
|     vi.spyOn(svg, 'append').mockReturnValue(fauxTitle); | ||||
|     const titleAttrSpy = vi.spyOn(fauxTitle, 'attr'); | ||||
|  | ||||
|   jsdomIt('class is the given css class', ({ svg }) => { | ||||
|     utils.insertTitle(svg, 'testClass', 5, 'test title'); | ||||
|     expect(titleAttrSpy).toHaveBeenCalledWith('class', 'testClass'); | ||||
|     const titleNode = ensureNodeFromSelector('svg > text'); | ||||
|     expect(titleNode.getAttribute('class')).toBe('testClass'); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
|   | ||||
							
								
								
									
										454
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										454
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -16,7 +16,6 @@ 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'], | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Sidharth Vinod
					Sidharth Vinod