diff --git a/packages/mermaid/package.json b/packages/mermaid/package.json index 02edc874b..12da8c2ff 100644 --- a/packages/mermaid/package.json +++ b/packages/mermaid/package.json @@ -87,6 +87,7 @@ "coveralls": "^3.1.1", "cpy-cli": "^4.2.0", "cspell": "^6.14.3", + "csstree-validator": "^3.0.0", "globby": "^13.1.2", "jison": "^0.4.18", "js-base64": "^3.7.2", diff --git a/packages/mermaid/src/styles.spec.ts b/packages/mermaid/src/styles.spec.ts new file mode 100644 index 000000000..07997ad60 --- /dev/null +++ b/packages/mermaid/src/styles.spec.ts @@ -0,0 +1,120 @@ +import { vi } from 'vitest'; + +// @ts-expect-error This module has no TypeScript types +import { validate } from 'csstree-validator'; +import { compile, serialize, stringify } from 'stylis'; + +import { getConfig } from './config'; +import theme from './themes'; + +/** + * Import the getStyles function from each diagram. + * + * Unfortunately, we can't use the `diagrams/*?/*Detector.ts` functions, + * because many of the diagrams have a circular dependency import error + * (they import mermaidAPI.js, which imports diagramOrchestrator.js, which causes a loop) + */ +import c4 from './diagrams/c4/styles'; +import classDiagram from './diagrams/class/styles'; +import flowchart from './diagrams/flowchart/styles'; +import flowchartElk from './diagrams/flowchart/elk/styles'; +import er from './diagrams/er/styles'; +import error from './diagrams/error/styles'; +import git from './diagrams/git/styles'; +import gantt from './diagrams/gantt/styles'; +import info from './diagrams/info/styles'; +import pie from './diagrams/pie/styles'; +import requirement from './diagrams/requirement/styles'; +import sequence from './diagrams/sequence/styles'; +import state from './diagrams/state/styles'; +import journey from './diagrams/user-journey/styles'; +import timeline from './diagrams/timeline/styles'; +import mindmap from './diagrams/mindmap/styles'; +import themes from './themes'; + +async function checkValidStylisCSSStyleSheet(stylisString: string) { + const cssString = serialize(compile(`#my-svg-id{${stylisString}}`), stringify); + const errors = validate(cssString, 'this-file-was-created-by-tests.css') as Error[]; + + const unexpectedErrors = errors.filter((error) => { + const cssErrorsToIgnore = [ + // Valid in SVG2, see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/rx + // Ideally, we'd remove this, since some browsers do not support SVG2. + 'Unknown property `rx`', + // Valid in SVG2, see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/ry + 'Unknown property `ry`', + // TODO: I'm pretty sure that even in SVG2, this isn't allowed to be a CSS + // attribute. + 'Unknown property `dy`', + ]; + return !cssErrorsToIgnore.some((cssErrorToIgnore) => error.message.match(cssErrorToIgnore)); + }); + + if (unexpectedErrors.length > 0) { + throw new Error( + `The given CSS string was invalid: ${errors}.\n\n` + + 'Copy the below CSS into https://jigsaw.w3.org/css-validator/validator to help debug where the invalid CSS is:\n\n' + + `Original CSS value was ${cssString}` + ); + } +} + +describe('styles', () => { + beforeEach(() => { + // resets the styles added to addStylesForDiagram() + vi.resetModules(); + }); + + describe('getStyles', () => { + test('should return a valid style for an empty type', async () => { + const { default: getStyles, addStylesForDiagram } = await import('./styles'); + + const diagramType = 'my-custom-mocked-type-with-no-styles'; + const myTypeGetStylesFunc = vi.fn().mockReturnValue(''); + + addStylesForDiagram(diagramType, myTypeGetStylesFunc); + + const styles = getStyles(diagramType, '', getConfig().themeVariables); + + await checkValidStylisCSSStyleSheet(styles); + }); + + /** + * Test CSS for each diagram type and each theme. + */ + for (const themeId of Object.keys(theme) as (keyof typeof theme)[]) { + for (const [diagramId, getDiagramStyles] of Object.entries({ + c4, + classDiagram, + er, + error, + flowchart, + flowchartElk, + gantt, + git, + info, + journey, + mindmap, + pie, + requirement, + sequence, + state, + timeline, + })) { + test(`should return a valid style for diagram ${diagramId} and theme ${themeId}`, async () => { + const { default: getStyles, addStylesForDiagram } = await import('./styles'); + + addStylesForDiagram(diagramId, getDiagramStyles); + const styles = getStyles( + diagramId, + '', + // @ts-expect-error This will probably be broken until we create a proper Themes type. + themes[themeId].getThemeVariables() + ); + + await checkValidStylisCSSStyleSheet(styles); + }); + } + } + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bdac6ed51..d3e978513 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -138,6 +138,7 @@ importers: coveralls: ^3.1.1 cpy-cli: ^4.2.0 cspell: ^6.14.3 + csstree-validator: ^3.0.0 cytoscape: ^3.23.0 cytoscape-cose-bilkent: ^4.1.0 cytoscape-fcose: ^2.1.0 @@ -206,6 +207,7 @@ importers: coveralls: 3.1.1 cpy-cli: 4.2.0 cspell: 6.14.3 + csstree-validator: 3.0.0 globby: 13.1.2 jison: 0.4.18 js-base64: 3.7.2 @@ -4065,7 +4067,7 @@ packages: /axios/0.21.4_debug@4.3.2: resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==} dependencies: - follow-redirects: 1.15.2_debug@4.3.4 + follow-redirects: 1.15.2_debug@4.3.2 transitivePeerDependencies: - debug dev: true @@ -4509,6 +4511,13 @@ packages: jsonlint: 1.6.0 dev: true + /clap/3.1.1: + resolution: {integrity: sha512-vp42956Ax06WwaaheYEqEOgXZ3VKJxgccZ0gJL0HpyiupkIS9RVJFo5eDU1BPeQAOqz+cclndZg4DCqG1sJReQ==} + engines: {node: ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + dependencies: + ansi-colors: 4.1.3 + dev: true + /clean-regexp/1.0.0: resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} engines: {node: '>=4'} @@ -5142,6 +5151,14 @@ packages: source-map: 0.6.1 dev: true + /css-tree/2.3.1: + resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + dependencies: + mdn-data: 2.0.30 + source-map-js: 1.0.2 + dev: true + /cssom/0.3.8: resolution: {integrity: sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==} dev: true @@ -5157,6 +5174,16 @@ packages: cssom: 0.3.8 dev: true + /csstree-validator/3.0.0: + resolution: {integrity: sha512-Y5OSq3wI0Xz6L7DCgJQtQ97U+v99SkX9r663VjpvUMJPhEr0A149OxiAGqcnokB5bt81irgnMudspBzujzqn0w==} + engines: {node: ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + hasBin: true + dependencies: + clap: 3.1.1 + css-tree: 2.3.1 + resolve: 1.22.1 + dev: true + /csstype/2.6.21: resolution: {integrity: sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==} dev: true @@ -6641,6 +6668,18 @@ packages: resolution: {integrity: sha512-XGozTsMPYkm+6b5QL3Z9wQcJjNYxp0CYn3U1gO7dwD6PAqU1SVWZxI9CCg3z+ml3YfqdPnrBehaBrnH2AGKbNA==} dev: true + /follow-redirects/1.15.2_debug@4.3.2: + resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dependencies: + debug: 4.3.2 + dev: true + /follow-redirects/1.15.2_debug@4.3.4: resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==} engines: {node: '>=4.0'} @@ -8728,6 +8767,10 @@ packages: resolution: {integrity: sha512-n4Vypz/DZgwo0iMHLQL49dJzlp7YtAJP+N07MZHpjPf/5XJuHUWstviF4Mn2jEiR/GNmtnRRqnwsXExk3igfFA==} dev: true + /mdn-data/2.0.30: + resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} + dev: true + /mdn-data/2.0.6: resolution: {integrity: sha512-rQvjv71olwNHgiTbfPZFkJtjNMciWgswYeciZhtvWLO8bmX3TnhyA62I6sTWOyZssWHJJjY6/KiWwqQsWWsqOA==} dev: true