From 228bafa909bec100ebec9b94fe42765a6c738e3a Mon Sep 17 00:00:00 2001 From: Yokozuna59 Date: Tue, 27 Jun 2023 18:07:35 +0300 Subject: [PATCH] refactor accessibility - remove @ts-ignore comments - rename vitest test and describe title - remove unnecessary types, e.i, `null` and `number` - clean `addSVGa11yTitleDescription` and `setA11yDiagramInfo` functions --- packages/mermaid/src/accessibility.spec.ts | 146 ++++++++++----------- packages/mermaid/src/accessibility.ts | 49 ++++--- packages/mermaid/src/tests/MockedD3.ts | 11 +- 3 files changed, 94 insertions(+), 112 deletions(-) diff --git a/packages/mermaid/src/accessibility.spec.ts b/packages/mermaid/src/accessibility.spec.ts index 7a3ab7f10..7745e02ef 100644 --- a/packages/mermaid/src/accessibility.spec.ts +++ b/packages/mermaid/src/accessibility.spec.ts @@ -1,27 +1,24 @@ import { MockedD3 } from './tests/MockedD3.js'; import { setA11yDiagramInfo, addSVGa11yTitleDescription } from './accessibility.js'; -import { D3Element } from './mermaidAPI.js'; +import type { D3Element } from './mermaidAPI.js'; describe('accessibility', () => { - const fauxSvgNode = new MockedD3(); + const fauxSvgNode: MockedD3 = new MockedD3(); describe('setA11yDiagramInfo', () => { - it('sets the svg element role to "graphics-document document"', () => { - // @ts-ignore Required to easily handle the d3 select types + 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'); }); - it('sets the aria-roledescription to the diagram type', () => { - // @ts-ignore Required to easily handle the d3 select types + 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'); }); - it('does not set the aria-roledescription if the diagram type is empty', () => { - // @ts-ignore Required to easily handle the d3 select types + 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); @@ -32,8 +29,8 @@ describe('accessibility', () => { describe('addSVGa11yTitleDescription', () => { const givenId = 'theBaseId'; - describe('with the given svg d3 object:', () => { - it('does nothing if there is no insert defined', () => { + describe('with svg d3 object', () => { + it('should do nothing if there is no insert defined', () => { const noInsertSvg = { attr: vi.fn(), }; @@ -42,26 +39,25 @@ describe('accessibility', () => { expect(noInsertAttrSpy).not.toHaveBeenCalled(); }); - // ---------------- - // Convenience functions to DRY up the spec + // convenience functions to DRY up the spec - function expectAriaLabelledByIsTitleId( + function expectAriaLabelledByItTitleId( svgD3Node: D3Element, - title: string | null | undefined, - desc: string | null | undefined, + 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 expectAriaDescribedByIsDescId( + function expectAriaDescribedByItDescId( svgD3Node: D3Element, - title: string | null | undefined, - desc: string | null | undefined, + 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}`); @@ -69,154 +65,148 @@ describe('accessibility', () => { function a11yTitleTagInserted( svgD3Node: D3Element, - title: string | null | undefined, - desc: string | null | undefined, + 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 | null | undefined, - desc: string | null | undefined, + 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 | null | undefined, - desc: string | null | undefined, + _svgD3Node: D3Element, + title: string | undefined, + desc: string | undefined, givenId: string, callNumber: number, expectedPrefix: string, - expectedText: string | null | undefined - ) { - const fauxInsertedD3 = new MockedD3(); - const svgInsertSpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxInsertedD3); - // @ts-ignore Required to easily handle the d3 select types + 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(svgInsertSpy).toHaveBeenCalledWith(expectedPrefix, ':first-child'); + expect(svginsertpy).toHaveBeenCalledWith(expectedPrefix, ':first-child'); expect(titleAttrSpy).toHaveBeenCalledWith('id', `chart-${expectedPrefix}-${givenId}`); expect(titleTextSpy).toHaveBeenNthCalledWith(callNumber, expectedText); } - // ---------------- - describe('given an a11y title', () => { + describe('with a11y title', () => { const a11yTitle = 'a11y title'; - describe('given an a11y description', () => { + describe('with a11y description', () => { const a11yDesc = 'a11y description'; - it('sets aria-labelledby to the title id inserted as a child', () => { - expectAriaLabelledByIsTitleId(fauxSvgNode, a11yTitle, a11yDesc, givenId); + it('shold set aria-labelledby to the title id inserted as a child', () => { + expectAriaLabelledByItTitleId(fauxSvgNode, a11yTitle, a11yDesc, givenId); }); - it('sets aria-describedby to the description id inserted as a child', () => { - expectAriaDescribedByIsDescId(fauxSvgNode, a11yTitle, a11yDesc, givenId); + it('should set aria-describedby to the description id inserted as a child', () => { + expectAriaDescribedByItDescId(fauxSvgNode, a11yTitle, a11yDesc, givenId); }); - it('inserts a title tag as the first child with the text set to the accTitle given', () => { + it('should insert title tag as the first child with the text set to the accTitle given', () => { a11yTitleTagInserted(fauxSvgNode, a11yTitle, a11yDesc, givenId, 2); }); - it('inserts a desc tag as the 2nd child with the text set to accDescription given', () => { + it('should insert desc tag as the 2nd child with the text set to accDescription given', () => { a11yDescTagInserted(fauxSvgNode, a11yTitle, a11yDesc, givenId, 1); }); }); - describe(`no a11y description`, () => { + describe(`without a11y description`, () => { const a11yDesc = undefined; - it('sets aria-labelledby to the title id inserted as a child', () => { - expectAriaLabelledByIsTitleId(fauxSvgNode, a11yTitle, a11yDesc, givenId); + it('should set aria-labelledby to the title id inserted as a child', () => { + expectAriaLabelledByItTitleId(fauxSvgNode, a11yTitle, a11yDesc, givenId); }); - it('no aria-describedby is set', () => { - // @ts-ignore Required to easily handle the d3 select types + 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()); }); - it('inserts a title tag as the first child with the text set to the accTitle given', () => { + it('should insert title tag as the first child with the text set to the accTitle given', () => { a11yTitleTagInserted(fauxSvgNode, a11yTitle, a11yDesc, givenId, 1); }); - it('no description tag is inserted', () => { - const fauxTitle = new MockedD3(); - const svgInsertSpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxTitle); + 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(svgInsertSpy).not.toHaveBeenCalledWith('desc', ':first-child'); + expect(svginsertpy).not.toHaveBeenCalledWith('desc', ':first-child'); }); }); }); - describe('no a11y title', () => { + describe('without a11y title', () => { const a11yTitle = undefined; - describe('given an a11y description', () => { + describe('with a11y description', () => { const a11yDesc = 'a11y description'; - it('no aria-labelledby is set', () => { - // @ts-ignore Required to easily handle the d3 select types + 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()); }); - it('no title tag inserted', () => { - const fauxTitle = new MockedD3(); - const svgInsertSpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxTitle); + 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(svgInsertSpy).not.toHaveBeenCalledWith('title', ':first-child'); + expect(svginsertpy).not.toHaveBeenCalledWith('title', ':first-child'); }); - it('sets aria-describedby to the description id inserted as a child', () => { - expectAriaDescribedByIsDescId(fauxSvgNode, a11yTitle, a11yDesc, givenId); + it('should set aria-describedby to the description id inserted as a child', () => { + expectAriaDescribedByItDescId(fauxSvgNode, a11yTitle, a11yDesc, givenId); }); - it('inserts a desc tag as the 2nd child with the text set to accDescription given', () => { + it('should insert desc tag as the 2nd child with the text set to accDescription given', () => { a11yDescTagInserted(fauxSvgNode, a11yTitle, a11yDesc, givenId, 1); }); }); - describe('no a11y description', () => { + describe('without a11y description', () => { const a11yDesc = undefined; - it('no aria-labelledby is set', () => { - // @ts-ignore Required to easily handle the d3 select types + 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()); }); - it('no aria-describedby is set', () => { - // @ts-ignore Required to easily handle the d3 select types + 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()); }); - it('no title tag inserted', () => { - const fauxTitle = new MockedD3(); - const svgInsertSpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxTitle); + 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(svgInsertSpy).not.toHaveBeenCalledWith('title', ':first-child'); + expect(svginsertpy).not.toHaveBeenCalledWith('title', ':first-child'); }); - it('no description tag inserted', () => { - const fauxDesc = new MockedD3(); - const svgInsertSpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxDesc); + 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(svgInsertSpy).not.toHaveBeenCalledWith('desc', ':first-child'); + expect(svginsertpy).not.toHaveBeenCalledWith('desc', ':first-child'); }); }); }); diff --git a/packages/mermaid/src/accessibility.ts b/packages/mermaid/src/accessibility.ts index eba3ba9a7..69e22b301 100644 --- a/packages/mermaid/src/accessibility.ts +++ b/packages/mermaid/src/accessibility.ts @@ -1,13 +1,11 @@ /** - * Accessibility (a11y) functions, types, helpers + * Accessibility (a11y) functions, types, helpers. + * * @see https://www.w3.org/WAI/ * @see https://www.w3.org/TR/wai-aria-1.1/ * @see https://www.w3.org/TR/svg-aam-1.0/ - * */ -import { D3Element } from './mermaidAPI.js'; - -import isEmpty from 'lodash-es/isEmpty.js'; +import type { D3Element } from './mermaidAPI.js'; /** * SVG element role: @@ -21,50 +19,47 @@ import isEmpty from 'lodash-es/isEmpty.js'; const SVG_ROLE = 'graphics-document document'; /** - * Add role and aria-roledescription to the svg element + * Add role and aria-roledescription to the svg element. * * @param svg - d3 object that contains the SVG HTML element * @param diagramType - diagram name for to the aria-roledescription */ -export function setA11yDiagramInfo(svg: D3Element, diagramType: string | null | undefined) { +export function setA11yDiagramInfo(svg: D3Element, diagramType: string) { svg.attr('role', SVG_ROLE); - if (!isEmpty(diagramType)) { + if (diagramType !== '') { svg.attr('aria-roledescription', diagramType); } } + /** * Add an accessible title and/or description element to a chart. * The title is usually not displayed and the description is never displayed. * - * The following charts display their title as a visual and accessibility element: gantt + * The following charts display their title as a visual and accessibility element: gantt. * * @param svg - d3 node to insert the a11y title and desc info - * @param a11yTitle - a11y title. null and undefined are meaningful: means to skip it - * @param a11yDesc - a11y description. null and undefined are meaningful: means to skip it + * @param a11yTitle - a11y title. undefined or empty strings mean to skip them + * @param a11yDesc - a11y description. undefined or empty strings mean to skip them * @param baseId - id used to construct the a11y title and description id */ export function addSVGa11yTitleDescription( svg: D3Element, - a11yTitle: string | null | undefined, - a11yDesc: string | null | undefined, + a11yTitle: string | undefined, + a11yDesc: string | undefined, baseId: string -) { +): void { if (svg.insert === undefined) { return; } - if (a11yTitle || a11yDesc) { - if (a11yDesc) { - const descId = 'chart-desc-' + baseId; - svg.attr('aria-describedby', descId); - svg.insert('desc', ':first-child').attr('id', descId).text(a11yDesc); - } - if (a11yTitle) { - const titleId = 'chart-title-' + baseId; - svg.attr('aria-labelledby', titleId); - svg.insert('title', ':first-child').attr('id', titleId).text(a11yTitle); - } - } else { - return; + if (a11yDesc) { + const descId = `chart-desc-${baseId}`; + svg.attr('aria-describedby', descId); + svg.insert('desc', ':first-child').attr('id', descId).text(a11yDesc); + } + if (a11yTitle) { + const titleId = `chart-title-${baseId}`; + svg.attr('aria-labelledby', titleId); + svg.insert('title', ':first-child').attr('id', titleId).text(a11yTitle); } } diff --git a/packages/mermaid/src/tests/MockedD3.ts b/packages/mermaid/src/tests/MockedD3.ts index 6d8d721e0..c5e080ba3 100644 --- a/packages/mermaid/src/tests/MockedD3.ts +++ b/packages/mermaid/src/tests/MockedD3.ts @@ -1,5 +1,3 @@ -import type {} from '@vitest/spy/dist/index.js'; - /** * 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. @@ -7,9 +5,8 @@ import type {} from '@vitest/spy/dist/index.js'; * 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(); + public attribs = new Map(); public id: string | undefined = ''; _children: MockedD3[] = []; @@ -72,9 +69,9 @@ export class MockedD3 { return newMock; }; - attr(attrName: string): null | undefined | string | number; - // attr(attrName: string, attrValue: string): MockedD3; - attr(attrName: string, attrValue?: string): null | undefined | string | number | MockedD3 { + 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 {