1/**
2 * Copyright 2018 Google Inc. All rights reserved.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *     http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17import expect from 'expect';
18import {
19  getTestState,
20  setupTestBrowserHooks,
21  setupTestPageAndContextHooks,
22  describeFailsFirefox,
23} from './mocha-utils'; // eslint-disable-line import/extensions
24
25describeFailsFirefox('Accessibility', function () {
26  setupTestBrowserHooks();
27  setupTestPageAndContextHooks();
28
29  it('should work', async () => {
30    const { page, isFirefox } = getTestState();
31
32    await page.setContent(`
33      <head>
34        <title>Accessibility Test</title>
35      </head>
36      <body>
37        <div>Hello World</div>
38        <h1>Inputs</h1>
39        <input placeholder="Empty input" autofocus />
40        <input placeholder="readonly input" readonly />
41        <input placeholder="disabled input" disabled />
42        <input aria-label="Input with whitespace" value="  " />
43        <input value="value only" />
44        <input aria-placeholder="placeholder" value="and a value" />
45        <div aria-hidden="true" id="desc">This is a description!</div>
46        <input aria-placeholder="placeholder" value="and a value" aria-describedby="desc" />
47        <select>
48          <option>First Option</option>
49          <option>Second Option</option>
50        </select>
51      </body>`);
52
53    await page.focus('[placeholder="Empty input"]');
54    const golden = isFirefox
55      ? {
56          role: 'document',
57          name: 'Accessibility Test',
58          children: [
59            { role: 'text leaf', name: 'Hello World' },
60            { role: 'heading', name: 'Inputs', level: 1 },
61            { role: 'entry', name: 'Empty input', focused: true },
62            { role: 'entry', name: 'readonly input', readonly: true },
63            { role: 'entry', name: 'disabled input', disabled: true },
64            { role: 'entry', name: 'Input with whitespace', value: '  ' },
65            { role: 'entry', name: '', value: 'value only' },
66            { role: 'entry', name: '', value: 'and a value' }, // firefox doesn't use aria-placeholder for the name
67            {
68              role: 'entry',
69              name: '',
70              value: 'and a value',
71              description: 'This is a description!',
72            }, // and here
73            {
74              role: 'combobox',
75              name: '',
76              value: 'First Option',
77              haspopup: true,
78              children: [
79                {
80                  role: 'combobox option',
81                  name: 'First Option',
82                  selected: true,
83                },
84                { role: 'combobox option', name: 'Second Option' },
85              ],
86            },
87          ],
88        }
89      : {
90          role: 'RootWebArea',
91          name: 'Accessibility Test',
92          children: [
93            { role: 'StaticText', name: 'Hello World' },
94            { role: 'heading', name: 'Inputs', level: 1 },
95            { role: 'textbox', name: 'Empty input', focused: true },
96            { role: 'textbox', name: 'readonly input', readonly: true },
97            { role: 'textbox', name: 'disabled input', disabled: true },
98            { role: 'textbox', name: 'Input with whitespace', value: '  ' },
99            { role: 'textbox', name: '', value: 'value only' },
100            { role: 'textbox', name: 'placeholder', value: 'and a value' },
101            {
102              role: 'textbox',
103              name: 'placeholder',
104              value: 'and a value',
105              description: 'This is a description!',
106            },
107            {
108              role: 'combobox',
109              name: '',
110              value: 'First Option',
111              children: [
112                { role: 'menuitem', name: 'First Option', selected: true },
113                { role: 'menuitem', name: 'Second Option' },
114              ],
115            },
116          ],
117        };
118    expect(await page.accessibility.snapshot()).toEqual(golden);
119  });
120  it('should report uninteresting nodes', async () => {
121    const { page, isFirefox } = getTestState();
122
123    await page.setContent(`<textarea>hi</textarea>`);
124    await page.focus('textarea');
125    const golden = isFirefox
126      ? {
127          role: 'entry',
128          name: '',
129          value: 'hi',
130          focused: true,
131          multiline: true,
132          children: [
133            {
134              role: 'text leaf',
135              name: 'hi',
136            },
137          ],
138        }
139      : {
140          role: 'textbox',
141          name: '',
142          value: 'hi',
143          focused: true,
144          multiline: true,
145          children: [
146            {
147              role: 'generic',
148              name: '',
149              children: [
150                {
151                  role: 'StaticText',
152                  name: 'hi',
153                },
154              ],
155            },
156          ],
157        };
158    expect(
159      findFocusedNode(
160        await page.accessibility.snapshot({ interestingOnly: false })
161      )
162    ).toEqual(golden);
163  });
164  it('roledescription', async () => {
165    const { page } = getTestState();
166
167    await page.setContent(
168      '<div tabIndex=-1 aria-roledescription="foo">Hi</div>'
169    );
170    const snapshot = await page.accessibility.snapshot();
171    expect(snapshot.children[0].roledescription).toEqual('foo');
172  });
173  it('orientation', async () => {
174    const { page } = getTestState();
175
176    await page.setContent(
177      '<a href="" role="slider" aria-orientation="vertical">11</a>'
178    );
179    const snapshot = await page.accessibility.snapshot();
180    expect(snapshot.children[0].orientation).toEqual('vertical');
181  });
182  it('autocomplete', async () => {
183    const { page } = getTestState();
184
185    await page.setContent('<input type="number" aria-autocomplete="list" />');
186    const snapshot = await page.accessibility.snapshot();
187    expect(snapshot.children[0].autocomplete).toEqual('list');
188  });
189  it('multiselectable', async () => {
190    const { page } = getTestState();
191
192    await page.setContent(
193      '<div role="grid" tabIndex=-1 aria-multiselectable=true>hey</div>'
194    );
195    const snapshot = await page.accessibility.snapshot();
196    expect(snapshot.children[0].multiselectable).toEqual(true);
197  });
198  it('keyshortcuts', async () => {
199    const { page } = getTestState();
200
201    await page.setContent(
202      '<div role="grid" tabIndex=-1 aria-keyshortcuts="foo">hey</div>'
203    );
204    const snapshot = await page.accessibility.snapshot();
205    expect(snapshot.children[0].keyshortcuts).toEqual('foo');
206  });
207  describe('filtering children of leaf nodes', function () {
208    it('should not report text nodes inside controls', async () => {
209      const { page, isFirefox } = getTestState();
210
211      await page.setContent(`
212        <div role="tablist">
213          <div role="tab" aria-selected="true"><b>Tab1</b></div>
214          <div role="tab">Tab2</div>
215        </div>`);
216      const golden = isFirefox
217        ? {
218            role: 'document',
219            name: '',
220            children: [
221              {
222                role: 'pagetab',
223                name: 'Tab1',
224                selected: true,
225              },
226              {
227                role: 'pagetab',
228                name: 'Tab2',
229              },
230            ],
231          }
232        : {
233            role: 'RootWebArea',
234            name: '',
235            children: [
236              {
237                role: 'tab',
238                name: 'Tab1',
239                selected: true,
240              },
241              {
242                role: 'tab',
243                name: 'Tab2',
244              },
245            ],
246          };
247      expect(await page.accessibility.snapshot()).toEqual(golden);
248    });
249    it('rich text editable fields should have children', async () => {
250      const { page, isFirefox } = getTestState();
251
252      await page.setContent(`
253        <div contenteditable="true">
254          Edit this image: <img src="fakeimage.png" alt="my fake image">
255        </div>`);
256      const golden = isFirefox
257        ? {
258            role: 'section',
259            name: '',
260            children: [
261              {
262                role: 'text leaf',
263                name: 'Edit this image: ',
264              },
265              {
266                role: 'StaticText',
267                name: 'my fake image',
268              },
269            ],
270          }
271        : {
272            role: 'generic',
273            name: '',
274            value: 'Edit this image: ',
275            children: [
276              {
277                role: 'StaticText',
278                name: 'Edit this image:',
279              },
280              {
281                role: 'img',
282                name: 'my fake image',
283              },
284            ],
285          };
286      const snapshot = await page.accessibility.snapshot();
287      expect(snapshot.children[0]).toEqual(golden);
288    });
289    it('rich text editable fields with role should have children', async () => {
290      const { page, isFirefox } = getTestState();
291
292      await page.setContent(`
293        <div contenteditable="true" role='textbox'>
294          Edit this image: <img src="fakeimage.png" alt="my fake image">
295        </div>`);
296      const golden = isFirefox
297        ? {
298            role: 'entry',
299            name: '',
300            value: 'Edit this image: my fake image',
301            children: [
302              {
303                role: 'StaticText',
304                name: 'my fake image',
305              },
306            ],
307          }
308        : {
309            role: 'textbox',
310            name: '',
311            value: 'Edit this image: ',
312            multiline: true,
313            children: [
314              {
315                role: 'StaticText',
316                name: 'Edit this image:',
317              },
318              {
319                role: 'img',
320                name: 'my fake image',
321              },
322            ],
323          };
324      const snapshot = await page.accessibility.snapshot();
325      expect(snapshot.children[0]).toEqual(golden);
326    });
327
328    // Firefox does not support contenteditable="plaintext-only".
329    describeFailsFirefox('plaintext contenteditable', function () {
330      it('plain text field with role should not have children', async () => {
331        const { page } = getTestState();
332
333        await page.setContent(`
334          <div contenteditable="plaintext-only" role='textbox'>Edit this image:<img src="fakeimage.png" alt="my fake image"></div>`);
335        const snapshot = await page.accessibility.snapshot();
336        expect(snapshot.children[0]).toEqual({
337          role: 'textbox',
338          name: '',
339          value: 'Edit this image:',
340          multiline: true,
341        });
342      });
343    });
344    it('non editable textbox with role and tabIndex and label should not have children', async () => {
345      const { page, isFirefox } = getTestState();
346
347      await page.setContent(`
348        <div role="textbox" tabIndex=0 aria-checked="true" aria-label="my favorite textbox">
349          this is the inner content
350          <img alt="yo" src="fakeimg.png">
351        </div>`);
352      const golden = isFirefox
353        ? {
354            role: 'entry',
355            name: 'my favorite textbox',
356            value: 'this is the inner content yo',
357          }
358        : {
359            role: 'textbox',
360            name: 'my favorite textbox',
361            value: 'this is the inner content ',
362          };
363      const snapshot = await page.accessibility.snapshot();
364      expect(snapshot.children[0]).toEqual(golden);
365    });
366    it('checkbox with and tabIndex and label should not have children', async () => {
367      const { page, isFirefox } = getTestState();
368
369      await page.setContent(`
370        <div role="checkbox" tabIndex=0 aria-checked="true" aria-label="my favorite checkbox">
371          this is the inner content
372          <img alt="yo" src="fakeimg.png">
373        </div>`);
374      const golden = isFirefox
375        ? {
376            role: 'checkbutton',
377            name: 'my favorite checkbox',
378            checked: true,
379          }
380        : {
381            role: 'checkbox',
382            name: 'my favorite checkbox',
383            checked: true,
384          };
385      const snapshot = await page.accessibility.snapshot();
386      expect(snapshot.children[0]).toEqual(golden);
387    });
388    it('checkbox without label should not have children', async () => {
389      const { page, isFirefox } = getTestState();
390
391      await page.setContent(`
392        <div role="checkbox" aria-checked="true">
393          this is the inner content
394          <img alt="yo" src="fakeimg.png">
395        </div>`);
396      const golden = isFirefox
397        ? {
398            role: 'checkbutton',
399            name: 'this is the inner content yo',
400            checked: true,
401          }
402        : {
403            role: 'checkbox',
404            name: 'this is the inner content yo',
405            checked: true,
406          };
407      const snapshot = await page.accessibility.snapshot();
408      expect(snapshot.children[0]).toEqual(golden);
409    });
410
411    describe('root option', function () {
412      it('should work a button', async () => {
413        const { page } = getTestState();
414
415        await page.setContent(`<button>My Button</button>`);
416
417        const button = await page.$<HTMLButtonElement>('button');
418        expect(await page.accessibility.snapshot({ root: button })).toEqual({
419          role: 'button',
420          name: 'My Button',
421        });
422      });
423      it('should work an input', async () => {
424        const { page } = getTestState();
425
426        await page.setContent(`<input title="My Input" value="My Value">`);
427
428        const input = await page.$('input');
429        expect(await page.accessibility.snapshot({ root: input })).toEqual({
430          role: 'textbox',
431          name: 'My Input',
432          value: 'My Value',
433        });
434      });
435      it('should work a menu', async () => {
436        const { page } = getTestState();
437
438        await page.setContent(`
439            <div role="menu" title="My Menu">
440              <div role="menuitem">First Item</div>
441              <div role="menuitem">Second Item</div>
442              <div role="menuitem">Third Item</div>
443            </div>
444          `);
445
446        const menu = await page.$('div[role="menu"]');
447        expect(await page.accessibility.snapshot({ root: menu })).toEqual({
448          role: 'menu',
449          name: 'My Menu',
450          children: [
451            { role: 'menuitem', name: 'First Item' },
452            { role: 'menuitem', name: 'Second Item' },
453            { role: 'menuitem', name: 'Third Item' },
454          ],
455        });
456      });
457      it('should return null when the element is no longer in DOM', async () => {
458        const { page } = getTestState();
459
460        await page.setContent(`<button>My Button</button>`);
461        const button = await page.$('button');
462        await page.$eval('button', (button) => button.remove());
463        expect(await page.accessibility.snapshot({ root: button })).toEqual(
464          null
465        );
466      });
467      it('should support the interestingOnly option', async () => {
468        const { page } = getTestState();
469
470        await page.setContent(`<div><button>My Button</button></div>`);
471        const div = await page.$('div');
472        expect(await page.accessibility.snapshot({ root: div })).toEqual(null);
473        expect(
474          await page.accessibility.snapshot({
475            root: div,
476            interestingOnly: false,
477          })
478        ).toEqual({
479          role: 'generic',
480          name: '',
481          children: [
482            {
483              role: 'button',
484              name: 'My Button',
485              children: [{ role: 'StaticText', name: 'My Button' }],
486            },
487          ],
488        });
489      });
490    });
491  });
492  function findFocusedNode(node) {
493    if (node.focused) return node;
494    for (const child of node.children || []) {
495      const focusedChild = findFocusedNode(child);
496      if (focusedChild) return focusedChild;
497    }
498    return null;
499  }
500});
501