add error checking (empty diagramType, title, desc) to a11y methods

This commit is contained in:
Ashley Engelund (weedySeaDragon @ github)
2022-11-17 12:21:45 -08:00
parent 03a11e103e
commit 4d7496b8dd
2 changed files with 194 additions and 84 deletions

View File

@@ -1,73 +1,172 @@
// Spec/tests for accessibility
import { MockedD3 } from './tests/MockedD3';
import { setA11yDiagramInfo, addSVGa11yTitleDescription } from './accessibility';
import { MockedD3 } from './tests/MockedD3';
describe('accessibility', () => {
const fauxSvgNode = new MockedD3();
const fauxSvgNode = new MockedD3();
const MockedDiagramDb = {
getAccTitle: vi.fn().mockReturnValue('the title'),
getAccDescription: vi.fn().mockReturnValue('the description'),
};
describe('setA11yDiagramInfo', () => {
it('sets the aria-roledescription to the diagram type', () => {
// @ts-ignore Required to easily handle the d3 select types
const svg_attr_spy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
setA11yDiagramInfo(fauxSvgNode, 'flowchart');
expect(svg_attr_spy).toHaveBeenCalledWith('aria-roledescription', 'flowchart');
});
});
describe('addSVGa11yTitleDescription', () => {
const testDiagramDb = MockedDiagramDb;
const givenId = 'theBaseId';
describe('with the given svg d3 object:', () => {
it('does nothing if there is no insert defined', () => {
const noInsertSvg = {
attr: vi.fn(),
};
const noInsert_attr_spy = vi.spyOn(noInsertSvg, 'attr').mockReturnValue(noInsertSvg);
addSVGa11yTitleDescription(testDiagramDb, noInsertSvg, givenId);
expect(noInsert_attr_spy).not.toHaveBeenCalled();
});
it('sets aria-labelledby to the title id and the description id inserted as children', () => {
// @ts-ignore Required to easily handle the d3 select types
describe('setA11yDiagramInfo', () => {
it('sets the aria-roledescription to the diagram type', () => {
// @ts-ignore Required to easily handle the d3 select types
const svg_attr_spy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
addSVGa11yTitleDescription(testDiagramDb, fauxSvgNode, givenId);
expect(svg_attr_spy).toHaveBeenCalledWith(
'aria-labelledby',
`chart-title-${givenId} chart-desc-${givenId}`
);
setA11yDiagramInfo(fauxSvgNode, 'flowchart');
expect(svg_attr_spy).toHaveBeenCalledWith('aria-roledescription', 'flowchart');
});
it('inserts a title tag as the first child with the text set to the accTitle returned by the diagram db', () => {
const faux_title = new MockedD3();
const svg_insert_spy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(faux_title);
it('does nothing if the diagram type is empty', () => {
// @ts-ignore Required to easily handle the d3 select types
const title_attr_spy = vi.spyOn(faux_title, 'attr').mockReturnValue(faux_title);
const title_text_spy = vi.spyOn(faux_title, 'text');
addSVGa11yTitleDescription(testDiagramDb, fauxSvgNode, givenId);
expect(svg_insert_spy).toHaveBeenCalledWith('title', ':first-child');
expect(title_attr_spy).toHaveBeenCalledWith('id', `chart-title-` + givenId);
expect(title_text_spy).toHaveBeenNthCalledWith(2, 'the title');
const svg_attr_spy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
setA11yDiagramInfo(fauxSvgNode, '');
expect(svg_attr_spy).not.toHaveBeenCalled();
});
});
it('inserts a desc tag as the 2nd child with the text set to accDescription returned by the diagram db', () => {
const faux_desc = new MockedD3();
const svg_insert_spy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(faux_desc);
// @ts-ignore Required to easily handle the d3 select types
const desc_attr_spy = vi.spyOn(faux_desc, 'attr').mockReturnValue(faux_desc);
const desc_text_spy = vi.spyOn(faux_desc, 'text');
describe('addSVGa11yTitleDescription', () => {
const givenId = 'theBaseId';
addSVGa11yTitleDescription(testDiagramDb, fauxSvgNode, givenId);
expect(svg_insert_spy).toHaveBeenCalledWith('desc', ':first-child');
expect(desc_attr_spy).toHaveBeenCalledWith('id', `chart-desc-` + givenId);
expect(desc_text_spy).toHaveBeenNthCalledWith(1, 'the description');
describe('with the given svg d3 object:', () => {
it('does nothing if there is no insert defined', () => {
const noInsertSvg = {
attr: vi.fn(),
};
const noInsert_attr_spy = vi.spyOn(noInsertSvg, 'attr').mockReturnValue(noInsertSvg);
addSVGa11yTitleDescription(noInsertSvg, 'some title', 'some desc', givenId);
expect(noInsert_attr_spy).not.toHaveBeenCalled();
});
describe('given an a11y title', () => {
const a11yTitle = 'a11y title';
describe('given an a11y description', () => {
const a11yDesc = 'a11y description';
it('sets aria-labelledby to the title id and the description id inserted as children', () => {
// @ts-ignore Required to easily handle the d3 select types
const svg_attr_spy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
expect(svg_attr_spy).toHaveBeenCalledWith(
'aria-labelledby',
`chart-title-${givenId} chart-desc-${givenId}`
);
});
it('inserts a title tag as the first child with the text set to the accTitle given', () => {
const faux_title = new MockedD3();
const svg_insert_spy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(faux_title);
// @ts-ignore Required to easily handle the d3 select types
const title_attr_spy = vi.spyOn(faux_title, 'attr').mockReturnValue(faux_title);
const title_text_spy = vi.spyOn(faux_title, 'text');
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
expect(svg_insert_spy).toHaveBeenCalledWith('desc', ':first-child');
expect(title_attr_spy).toHaveBeenCalledWith('id', `chart-desc-` + givenId);
expect(title_text_spy).toHaveBeenNthCalledWith(1, 'a11y description');
});
it('inserts a desc tag as the 2nd child with the text set to accDescription given', () => {
const faux_desc = new MockedD3();
const svg_insert_spy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(faux_desc);
// @ts-ignore Required to easily handle the d3 select types
const desc_attr_spy = vi.spyOn(faux_desc, 'attr').mockReturnValue(faux_desc);
const desc_text_spy = vi.spyOn(faux_desc, 'text');
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
expect(svg_insert_spy).toHaveBeenCalledWith('desc', ':first-child');
expect(desc_attr_spy).toHaveBeenCalledWith('id', `chart-desc-` + givenId);
expect(desc_text_spy).toHaveBeenNthCalledWith(1, 'a11y description');
});
});
describe(`no a11y description`, () => {
const a11yDesc = undefined;
it('sets aria-labelledby to the title id inserted as a child', () => {
// @ts-ignore Required to easily handle the d3 select types
const svg_attr_spy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
expect(svg_attr_spy).toHaveBeenCalledWith('aria-labelledby', `chart-title-${givenId}`);
});
it('inserts a title tag as the first child with the text set to the accTitle given', () => {
const faux_title = new MockedD3();
const svg_insert_spy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(faux_title);
// @ts-ignore Required to easily handle the d3 select types
const title_attr_spy = vi.spyOn(faux_title, 'attr').mockReturnValue(faux_title);
const title_text_spy = vi.spyOn(faux_title, 'text');
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
expect(svg_insert_spy).toHaveBeenCalledWith('title', ':first-child');
expect(title_attr_spy).toHaveBeenCalledWith('id', `chart-title-` + givenId);
expect(title_text_spy).toHaveBeenNthCalledWith(1, 'a11y title');
});
it('no description tag is inserted', () => {
const faux_title = new MockedD3();
const svg_insert_spy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(faux_title);
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
expect(svg_insert_spy).not.toHaveBeenCalledWith('desc', ':first-child');
});
});
});
describe('no a11y title', () => {
const a11yTitle = undefined;
describe('given an a11y description', () => {
const a11yDesc = 'a11y description';
it('no title tag inserted', () => {
const faux_title = new MockedD3();
const svg_insert_spy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(faux_title);
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
expect(svg_insert_spy).not.toHaveBeenCalledWith('title', ':first-child');
});
it('sets aria-labelledby to the description id inserted as a child', () => {
// @ts-ignore Required to easily handle the d3 select types
const svg_attr_spy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
expect(svg_attr_spy).toHaveBeenCalledWith('aria-labelledby', `chart-desc-${givenId}`);
});
it('inserts a desc tag as a child with the text set to accDescription given', () => {
const faux_desc = new MockedD3();
const svg_insert_spy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(faux_desc);
// @ts-ignore Required to easily handle the d3 select types
const desc_attr_spy = vi.spyOn(faux_desc, 'attr').mockReturnValue(faux_desc);
const desc_text_spy = vi.spyOn(faux_desc, 'text');
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
expect(svg_insert_spy).toHaveBeenCalledWith('desc', ':first-child');
expect(desc_attr_spy).toHaveBeenCalledWith('id', `chart-desc-` + givenId);
expect(desc_text_spy).toHaveBeenNthCalledWith(1, 'a11y description');
});
});
describe('no a11y description', () => {
const a11yDesc = undefined;
it('no title tag inserted', () => {
const faux_title = new MockedD3();
const svg_insert_spy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(faux_title);
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
expect(svg_insert_spy).not.toHaveBeenCalledWith('title', ':first-child');
});
it('no description tag inserted', () => {
const faux_desc = new MockedD3();
const svg_insert_spy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(faux_desc);
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
expect(svg_insert_spy).not.toHaveBeenCalledWith('desc', ':first-child');
});
it('no aria-labelledby is set', () => {
// @ts-ignore Required to easily handle the d3 select types
const svg_attr_spy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
expect(svg_attr_spy).not.toHaveBeenCalled();
});
});
});
});
});
});

View File

@@ -3,43 +3,54 @@
*
*/
// This is just a convenience alias to make it clear the type is a d3 object. (It's easier to make it 'any' instead of the comple typing set in d3)
import { isEmpty, compact } from 'lodash';
// This is just a convenience alias to make it clear the type is a d3 object. (It's easier to make it 'any' instead of the complete typing set in d3)
type D3object = any;
/**
* Set the accessibility (a11y) information for the svg d3 object using the given diagram type
* Note that the svg element role _should_ be mapped to a 'graphics-document' by default. Thus we don't set it here, but can set it in the future if needed.
* Add aria-roledescription to the svg element to the diagramType
*
* @param svg - d3 object that contains the SVG HTML element
* @param diagramType - diagram name for to the aria-roledescription
*/
export function setA11yDiagramInfo(svg: D3object, diagramType: string) {
svg.attr('aria-roledescription', diagramType);
export function setA11yDiagramInfo(svg: D3object, diagramType: string | null | undefined) {
if (!isEmpty(diagramType)) {
svg.attr('aria-roledescription', diagramType);
}
}
/**
* This method will add a basic title and description element to a chart. The yy parser will need to
* respond to getAccTitle and getAccDescription,
* where the accessible title is the title element on the chart.
* Add an accessible title and/or description element to a chart.
* The title is usually not displayed and the description is never displayed.
*
* Note that the accessible title is generally _not_ displayed
* and the accessible 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. TODO fix this
*
* @param diagramDb - the 'db' object/module for a diagram. Must respond to getAccTitle() and getAccDescription()
* @param svg - the d3 object that represents the svg element
* @param baseId - the id to use as the base for the title and description
* @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 baseId - id used to construct the a11y title and description id
*/
export function addSVGa11yTitleDescription(diagramDb: any, svg: D3object, baseId: string) {
export function addSVGa11yTitleDescription(
svg: D3object,
a11yTitle: string | null | undefined,
a11yDesc: string | null | undefined,
baseId: string
) {
if (typeof svg.insert === 'undefined') {
return;
}
const titleId = 'chart-title-' + baseId;
const descId = 'chart-desc-' + baseId;
svg.attr('aria-labelledby', titleId + ' ' + descId);
svg.insert('desc', ':first-child').attr('id', descId).text(diagramDb.getAccDescription());
svg.insert('title', ':first-child').attr('id', titleId).text(diagramDb.getAccTitle());
const titleId = a11yTitle ? 'chart-title-' + baseId : null;
const descId = a11yDesc ? 'chart-desc-' + baseId : null;
if (a11yTitle || a11yDesc) {
svg.attr('aria-labelledby', compact([titleId, descId]).join(' '));
if (a11yDesc) {
svg.insert('desc', ':first-child').attr('id', descId).text(a11yDesc);
}
if (a11yTitle) {
svg.insert('title', ':first-child').attr('id', titleId).text(a11yTitle);
}
} else {
return;
}
}