-
Notifications
You must be signed in to change notification settings - Fork 86
/
Copy patha11y.test.js
208 lines (180 loc) · 7.98 KB
/
a11y.test.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
import { expect } from '@esm-bundle/chai';
import { down, fixtureSync, focusin, isFirefox, keyboardEventFor } from '@vaadin/testing-helpers';
import sinon from 'sinon';
import '../vaadin-rich-text-editor.js';
describe('accessibility', () => {
'use strict';
const flushFormatAnnouncer = () => {
rte.__debounceAnnounceFormatting && rte.__debounceAnnounceFormatting.flush();
};
const flushValueDebouncer = () => rte.__debounceSetValue && rte.__debounceSetValue.flush();
let rte, content, buttons, announcer, editor;
beforeEach(() => {
rte = fixtureSync('<vaadin-rich-text-editor></vaadin-rich-text-editor>');
editor = rte._editor;
buttons = Array.from(rte.shadowRoot.querySelectorAll(`[part=toolbar] button`));
content = rte.shadowRoot.querySelector('[contenteditable]');
announcer = rte.shadowRoot.querySelector('[aria-live=polite]');
});
describe('screen readers', () => {
it('should have default tooltips for the buttons', () => {
buttons.forEach((button, index) => {
const expectedLabel = rte.i18n[Object.keys(rte.i18n)[index]];
const tooltip = rte.shadowRoot.querySelector(`[for="${button.id}"]`);
expect(tooltip.text).to.equal(expectedLabel);
});
});
it('should localize tooltips for the buttons', () => {
const defaultI18n = rte.i18n;
const localized = {};
Object.keys(defaultI18n).forEach((key) => {
localized[key] = `${defaultI18n[key]} localized`;
});
rte.i18n = localized;
buttons.forEach((button, index) => {
const expectedLabel = `${defaultI18n[Object.keys(defaultI18n)[index]]} localized`;
const tooltip = rte.shadowRoot.querySelector(`[for="${button.id}"]`);
expect(tooltip.text).to.equal(expectedLabel);
});
});
it('should have an invisible aria-live element', () => {
const clip = getComputedStyle(announcer).clip;
expect(clip).to.equal('rect(0px, 0px, 0px, 0px)');
});
it('should have textbox role', () => {
expect(content.getAttribute('role')).to.equal('textbox');
});
it('should be decorated with aria-multiline', () => {
expect(content.getAttribute('aria-multiline')).to.equal('true');
});
it('should announce the default formatting', () => {
rte.value = '[{"insert": "foo "}, {"attributes": {"bold": true}, "insert": "bar"}, {"insert": "\\n"}]';
editor.setSelection(1, 1);
flushFormatAnnouncer();
expect(announcer.textContent).to.equal('align left');
});
it('should announce custom formatting', (done) => {
rte.value = '[{"insert": "foo "}, {"attributes": {"bold": true}, "insert": "bar"}, {"insert": "\\n"}]';
editor.on('selection-change', () => {
flushFormatAnnouncer();
if (announcer.textContent === 'bold, align left') {
done();
}
});
editor.setSelection(5, 1);
});
it('should have role toolbar on the toolbar', () => {
const toolbar = rte.shadowRoot.querySelector('[part=toolbar]');
expect(toolbar.getAttribute('role')).to.be.equal('toolbar');
});
});
describe('keyboard navigation', () => {
it('should have only one tabbable button in toolbar', () => {
const tabbables = rte.shadowRoot.querySelectorAll(`[part=toolbar] button:not([tabindex="-1"])`);
expect(tabbables.length).to.equal(1);
expect(tabbables[0]).to.eql(buttons[0]);
});
it('should focus the next button on right-arrow', (done) => {
sinon.stub(buttons[0], 'focus').callsFake(done);
const e = keyboardEventFor('keydown', 39);
buttons[buttons.length - 1].dispatchEvent(e);
});
it('should focus the previous button on left-arrow', (done) => {
sinon.stub(buttons[buttons.length - 1], 'focus').callsFake(done);
const e = keyboardEventFor('keydown', 37);
buttons[0].dispatchEvent(e);
});
it('should change the tabbable button on arrow navigation', () => {
const e = keyboardEventFor('keydown', 39);
buttons[0].dispatchEvent(e);
expect(buttons[0].getAttribute('tabindex')).to.equal('-1');
expect(buttons[1].getAttribute('tabindex')).not.to.be.ok;
});
// This is a common pattern in popular rich text editors so users might expect
// the combo to work. The toolbar is still accessible with the tab key normally.
it('should focus a toolbar button on meta-f10 combo', (done) => {
sinon.stub(buttons[0], 'focus').callsFake(done);
editor.focus();
const e = keyboardEventFor('keydown', 121, ['alt']);
content.dispatchEvent(e);
});
it('should focus a toolbar button on shift-tab combo', (done) => {
sinon.stub(buttons[0], 'focus').callsFake(done);
editor.focus();
const e = keyboardEventFor('keydown', 9, ['shift']);
content.dispatchEvent(e);
});
it('should mark toolbar as focused before focusing it on shift-tab', (done) => {
const spy = sinon.spy(rte, '_markToolbarFocused');
sinon.stub(buttons[0], 'focus').callsFake(() => {
expect(spy.calledOnce).to.be.true;
done();
});
editor.focus();
const e = keyboardEventFor('keydown', 9, ['shift']);
content.dispatchEvent(e);
});
it('should prevent keydown and focus the editor on esc', (done) => {
sinon.stub(editor, 'focus').callsFake(done);
const e = new CustomEvent('keydown', { bubbles: true });
e.keyCode = 27;
const result = buttons[0].dispatchEvent(e);
expect(result).to.be.false; // DispatchEvent returns false when preventDefault is called
});
it('should prevent keydown and focus the editor on tab', (done) => {
sinon.stub(editor, 'focus').callsFake(done);
const e = new CustomEvent('keydown', { bubbles: true });
e.keyCode = 9;
e.shiftKey = false;
const result = buttons[0].dispatchEvent(e);
expect(result).to.be.false; // DispatchEvent returns false when preventDefault is called
});
it('should preserve the text selection on shift-tab', (done) => {
sinon.stub(buttons[0], 'focus').callsFake(() => {
expect(editor.getSelection()).to.deep.equal({ index: 0, length: 2 });
done();
});
rte.value = '[{"attributes":{"list":"bullet"},"insert":"Foo\\n"}]';
editor.focus();
editor.setSelection(0, 2);
const e = keyboardEventFor('keydown', 9, ['shift']);
content.dispatchEvent(e);
});
it('should change indentation and prevent shift-tab keydown in the code block', () => {
rte.value = '[{"insert":" foo"},{"attributes":{"code-block":true},"insert":"\\n"}]';
editor.focus();
editor.setSelection(2, 0);
const e = keyboardEventFor('keydown', 9, ['shift']);
content.dispatchEvent(e);
flushValueDebouncer();
expect(rte.value).to.equal('[{"insert":"foo"},{"attributes":{"code-block":true},"insert":"\\n"}]');
expect(e.defaultPrevented).to.be.true;
});
(isFirefox ? it : it.skip)('should focus the fake target on content focusin', (done) => {
const spy = sinon.spy(rte, '__createFakeFocusTarget');
sinon.stub(editor, 'focus').callsFake(() => {
expect(spy.calledOnce).to.be.true;
const fake = spy.firstCall.returnValue;
const style = getComputedStyle(fake);
expect(style.position).to.equal('absolute');
expect(style.left).to.equal('-9999px');
expect(style.top).to.equal(`${document.documentElement.scrollTop}px`);
done();
});
focusin(content);
});
(isFirefox ? it : it.skip)('should focus the fake target on mousedown when content is not focused', (done) => {
const spy = sinon.spy(rte, '__createFakeFocusTarget');
sinon.stub(editor, 'focus').callsFake(() => {
expect(spy.calledOnce).to.be.true;
const fake = spy.firstCall.returnValue;
const style = getComputedStyle(fake);
expect(style.position).to.equal('absolute');
expect(style.left).to.equal('-9999px');
expect(style.top).to.equal(`${document.documentElement.scrollTop}px`);
done();
});
down(content);
});
});
});