ipsos-components
Version:
Material Design components for Angular
502 lines (369 loc) • 18.1 kB
text/typescript
import {Platform} from '@angular/cdk/platform';
import {InteractivityChecker} from './interactivity-checker';
describe('InteractivityChecker', () => {
let testContainerElement: HTMLElement;
let checker: InteractivityChecker;
let platform: Platform = new Platform();
beforeEach(() => {
testContainerElement = document.createElement('div');
document.body.appendChild(testContainerElement);
checker = new InteractivityChecker(platform);
});
afterEach(() => {
document.body.removeChild(testContainerElement);
testContainerElement.innerHTML = '';
});
describe('isDisabled', () => {
it('should return true for disabled elements', () => {
let elements = createElements('input', 'textarea', 'select', 'button', 'mat-checkbox');
elements.forEach(el => el.setAttribute('disabled', ''));
appendElements(elements);
elements.forEach(el => {
expect(checker.isDisabled(el))
.toBe(true, `Expected <${el.nodeName} disabled> to be disabled`);
});
});
it('should return false for elements without disabled', () => {
let elements = createElements('input', 'textarea', 'select', 'button', 'mat-checkbox');
appendElements(elements);
elements.forEach(el => {
expect(checker.isDisabled(el))
.toBe(false, `Expected <${el.nodeName}> not to be disabled`);
});
});
});
describe('isVisible', () => {
it('should return false for a `display: none` element', () => {
testContainerElement.innerHTML =
`<input style="display: none;">`;
let input = testContainerElement.querySelector('input') as HTMLElement;
expect(checker.isVisible(input))
.toBe(false, 'Expected element with `display: none` to not be visible');
});
it('should return false for the child of a `display: none` element', () => {
testContainerElement.innerHTML =
`<div style="display: none;">
<input>
</div>`;
let input = testContainerElement.querySelector('input') as HTMLElement;
expect(checker.isVisible(input))
.toBe(false, 'Expected element with `display: none` parent to not be visible');
});
it('should return false for a `visibility: hidden` element', () => {
testContainerElement.innerHTML =
`<input style="visibility: hidden;">`;
let input = testContainerElement.querySelector('input') as HTMLElement;
expect(checker.isVisible(input))
.toBe(false, 'Expected element with `visibility: hidden` to not be visible');
});
it('should return false for the child of a `visibility: hidden` element', () => {
testContainerElement.innerHTML =
`<div style="visibility: hidden;">
<input>
</div>`;
let input = testContainerElement.querySelector('input') as HTMLElement;
expect(checker.isVisible(input))
.toBe(false, 'Expected element with `visibility: hidden` parent to not be visible');
});
it('should return true for an element with `visibility: hidden` ancestor and *closer* ' +
'`visibility: visible` ancestor', () => {
testContainerElement.innerHTML =
`<div style="visibility: hidden;">
<div style="visibility: visible;">
<input>
</div>
</div>`;
let input = testContainerElement.querySelector('input') as HTMLElement;
expect(checker.isVisible(input))
.toBe(true, 'Expected element with `visibility: hidden` ancestor and closer ' +
'`visibility: visible` ancestor to be visible');
});
it('should return true for an element without visibility modifiers', () => {
let input = document.createElement('input');
testContainerElement.appendChild(input);
expect(checker.isVisible(input))
.toBe(true, 'Expected element without visibility modifiers to be visible');
});
});
describe('isFocusable', () => {
it('should return true for native form controls', () => {
let elements = createElements('input', 'textarea', 'select', 'button');
appendElements(elements);
elements.forEach(el => {
expect(checker.isFocusable(el)).toBe(true, `Expected <${el.nodeName}> to be focusable`);
});
});
it('should return true for an anchor with an href', () => {
let anchor = document.createElement('a');
anchor.href = 'google.com';
testContainerElement.appendChild(anchor);
expect(checker.isFocusable(anchor)).toBe(true, `Expected <a> with href to be focusable`);
});
it('should return false for an anchor without an href', () => {
let anchor = document.createElement('a');
testContainerElement.appendChild(anchor);
expect(checker.isFocusable(anchor))
.toBe(false, `Expected <a> without href not to be focusable`);
});
it('should return false for disabled form controls', () => {
let elements = createElements('input', 'textarea', 'select', 'button');
elements.forEach(el => el.setAttribute('disabled', ''));
appendElements(elements);
elements.forEach(el => {
expect(checker.isFocusable(el))
.toBe(false, `Expected <${el.nodeName} disabled> not to be focusable`);
});
});
it('should return false for a `display: none` element', () => {
testContainerElement.innerHTML =
`<input style="display: none;">`;
let input = testContainerElement.querySelector('input') as HTMLElement;
expect(checker.isFocusable(input))
.toBe(false, 'Expected element with `display: none` to not be visible');
});
it('should return false for the child of a `display: none` element', () => {
testContainerElement.innerHTML =
`<div style="display: none;">
<input>
</div>`;
let input = testContainerElement.querySelector('input') as HTMLElement;
expect(checker.isFocusable(input))
.toBe(false, 'Expected element with `display: none` parent to not be visible');
});
it('should return false for a `visibility: hidden` element', () => {
testContainerElement.innerHTML =
`<input style="visibility: hidden;">`;
let input = testContainerElement.querySelector('input') as HTMLElement;
expect(checker.isFocusable(input))
.toBe(false, 'Expected element with `visibility: hidden` not to be focusable');
});
it('should return false for the child of a `visibility: hidden` element', () => {
testContainerElement.innerHTML =
`<div style="visibility: hidden;">
<input>
</div>`;
let input = testContainerElement.querySelector('input') as HTMLElement;
expect(checker.isFocusable(input))
.toBe(false, 'Expected element with `visibility: hidden` parent not to be focusable');
});
it('should return true for an element with `visibility: hidden` ancestor and *closer* ' +
'`visibility: visible` ancestor', () => {
testContainerElement.innerHTML =
`<div style="visibility: hidden;">
<div style="visibility: visible;">
<input>
</div>
</div>`;
let input = testContainerElement.querySelector('input') as HTMLElement;
expect(checker.isFocusable(input))
.toBe(true, 'Expected element with `visibility: hidden` ancestor and closer ' +
'`visibility: visible` ancestor to be focusable');
});
it('should return false for an element with an empty tabindex', () => {
let element = document.createElement('div');
element.setAttribute('tabindex', '');
testContainerElement.appendChild(element);
expect(checker.isFocusable(element))
.toBe(false, `Expected element with tabindex="" not to be focusable`);
});
it('should return false for an element with a non-numeric tabindex', () => {
let element = document.createElement('div');
element.setAttribute('tabindex', 'abba');
testContainerElement.appendChild(element);
expect(checker.isFocusable(element))
.toBe(false, `Expected element with non-numeric tabindex not to be focusable`);
});
it('should return true for an element with contenteditable', () => {
let element = document.createElement('div');
element.setAttribute('contenteditable', '');
testContainerElement.appendChild(element);
expect(checker.isFocusable(element))
.toBe(true, `Expected element with contenteditable to be focusable`);
});
it('should return false for inert div and span', () => {
let elements = createElements('div', 'span');
appendElements(elements);
elements.forEach(el => {
expect(checker.isFocusable(el))
.toBe(false, `Expected <${el.nodeName}> not to be focusable`);
});
});
});
describe('isTabbable', () => {
it('should respect the tabindex for video elements with controls',
// Do not run for Blink, Firefox and iOS because those treat video elements
// with controls different and are covered in other tests.
runIf(!platform.BLINK && !platform.FIREFOX && !platform.IOS, () => {
let video = createFromTemplate('<video controls>', true);
expect(checker.isTabbable(video)).toBe(true);
video.tabIndex = -1;
expect(checker.isTabbable(video)).toBe(false);
})
);
it('should always mark video elements with controls as tabbable (BLINK & FIREFOX)',
// Only run this spec for Blink and Firefox, because those always treat video
// elements with controls as tabbable.
runIf(platform.BLINK || platform.FIREFOX, () => {
let video = createFromTemplate('<video controls>', true);
expect(checker.isTabbable(video)).toBe(true);
video.tabIndex = -1;
expect(checker.isTabbable(video)).toBe(true);
})
);
// Some tests should not run inside of iOS browsers, because those only allow specific
// elements to be tabbable and cause the tests to always fail.
describe('for non-iOS browsers', runIf(!platform.IOS, () => {
it('should mark form controls and anchors without tabindex attribute as tabbable', () => {
let elements = createElements('input', 'textarea', 'select', 'button', 'a');
appendElements(elements);
elements.forEach(el => {
expect(checker.isTabbable(el)).toBe(true, `Expected <${el.nodeName}> to be tabbable`);
});
});
it('should return true for div and span with tabindex == 0', () => {
let elements = createElements('div', 'span');
elements.forEach(el => el.setAttribute('tabindex', '0'));
appendElements(elements);
elements.forEach(el => {
expect(checker.isFocusable(el))
.toBe(true, `Expected <${el.nodeName} tabindex="0"> to be focusable`);
});
});
it('should return false for native form controls and anchor with tabindex == -1', () => {
let elements = createElements('input', 'textarea', 'select', 'button', 'a');
elements.forEach(el => el.setAttribute('tabindex', '-1'));
appendElements(elements);
elements.forEach(el => {
expect(checker.isTabbable(el))
.toBe(false, `Expected <${el.nodeName} tabindex="-1"> not to be tabbable`);
});
});
it('should return true for div and span with tabindex == 0', () => {
let elements = createElements('div', 'span');
elements.forEach(el => el.setAttribute('tabindex', '0'));
appendElements(elements);
elements.forEach(el => {
expect(checker.isTabbable(el))
.toBe(true, `Expected <${el.nodeName} tabindex="0"> to be tabbable`);
});
});
it('should respect the inherited tabindex inside of frame elements', () => {
let iframe = createFromTemplate('<iframe>', true) as HTMLFrameElement;
let button = createFromTemplate('<button tabindex="0">Not Tabbable</button>');
appendElements([iframe]);
iframe.setAttribute('tabindex', '-1');
iframe.contentDocument.body.appendChild(button);
expect(checker.isTabbable(iframe)).toBe(false);
expect(checker.isTabbable(button)).toBe(false);
iframe.removeAttribute('tabindex');
expect(checker.isTabbable(iframe)).toBe(false);
expect(checker.isTabbable(button)).toBe(true);
});
it('should mark elements which are contentEditable as tabbable', () => {
let editableEl = createFromTemplate('<div contenteditable="true">', true);
expect(checker.isTabbable(editableEl)).toBe(true);
editableEl.tabIndex = -1;
expect(checker.isTabbable(editableEl)).toBe(false);
});
it('should never mark iframe elements as tabbable', () => {
let iframe = createFromTemplate('<iframe>', true);
// iFrame elements will be never marked as tabbable, because it depends on the content
// which is mostly not detectable due to CORS and also the checks will be not reliable.
expect(checker.isTabbable(iframe)).toBe(false);
});
it('should always mark audio elements without controls as not tabbable', () => {
let audio = createFromTemplate('<audio>', true);
expect(checker.isTabbable(audio)).toBe(false);
});
}));
describe('for Blink and Webkit browsers', runIf(platform.BLINK || platform.WEBKIT, () => {
it('should not mark elements inside of object frames as tabbable', () => {
let objectEl = createFromTemplate('<object>', true) as HTMLObjectElement;
let button = createFromTemplate('<button tabindex="0">Not Tabbable</button>');
appendElements([objectEl]);
// This is a hack to create an empty contentDocument for the frame element.
objectEl.type = 'text/html';
objectEl.contentDocument.body.appendChild(button);
expect(checker.isTabbable(objectEl)).toBe(false);
expect(checker.isTabbable(button)).toBe(false);
});
it('should not mark elements inside of invisible frames as tabbable', () => {
let iframe = createFromTemplate('<iframe>', true) as HTMLFrameElement;
let button = createFromTemplate('<button tabindex="0">Not Tabbable</button>');
appendElements([iframe]);
iframe.style.display = 'none';
iframe.contentDocument.body.appendChild(button);
expect(checker.isTabbable(iframe)).toBe(false);
expect(checker.isTabbable(button)).toBe(false);
});
it('should never mark object frame elements as tabbable', () => {
let objectEl = createFromTemplate('<object>', true);
expect(checker.isTabbable(objectEl)).toBe(false);
});
}));
describe('for Blink browsers', runIf(platform.BLINK, () => {
it('should always mark audio elements with controls as tabbable', () => {
let audio = createFromTemplate('<audio controls>', true);
expect(checker.isTabbable(audio)).toBe(true);
audio.tabIndex = -1;
// The audio element will be still tabbable because Blink always
// considers them as tabbable.
expect(checker.isTabbable(audio)).toBe(true);
});
}));
describe('for Internet Explorer', runIf(platform.TRIDENT, () => {
it('should never mark video elements without controls as tabbable', () => {
// In Internet Explorer video elements without controls are never tabbable.
let video = createFromTemplate('<video>', true);
expect(checker.isTabbable(video)).toBe(false);
video.tabIndex = 0;
expect(checker.isTabbable(video)).toBe(false);
});
}));
describe('for iOS browsers', runIf(platform.IOS && platform.WEBKIT, () => {
it('should never allow div elements to be tabbable', () => {
let divEl = createFromTemplate('<div tabindex="0">', true);
expect(checker.isTabbable(divEl)).toBe(false);
});
it('should never allow span elements to be tabbable', () => {
let spanEl = createFromTemplate('<span tabindex="0">Text</span>', true);
expect(checker.isTabbable(spanEl)).toBe(false);
});
it('should never allow button elements to be tabbable', () => {
let buttonEl = createFromTemplate('<button tabindex="0">', true);
expect(checker.isTabbable(buttonEl)).toBe(false);
});
it('should never allow anchor elements to be tabbable', () => {
let anchorEl = createFromTemplate('<a tabindex="0">Link</a>', true);
expect(checker.isTabbable(anchorEl)).toBe(false);
});
}));
});
/** Creates an array of elements with the given node names. */
function createElements(...nodeNames: string[]) {
return nodeNames.map(name => document.createElement(name));
}
function createFromTemplate(template: string, append = false) {
let tmpRoot = document.createElement('div');
tmpRoot.innerHTML = template;
let element = tmpRoot.firstElementChild!;
tmpRoot.removeChild(element);
if (append) {
appendElements([element]);
}
return element as HTMLElement;
}
/** Appends elements to the testContainerElement. */
function appendElements(elements: Element[]) {
for (let e of elements) {
testContainerElement.appendChild(e);
}
}
function runIf(condition: boolean, runFn: Function): () => void {
return (...args: any[]) => {
if (condition) {
runFn.apply(this, args);
}
};
}
});