/**
* @license
* Copyright 2018 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import assert from
'assert' ;
import expect from
'expect' ;
import type {SerializedAXNode} from
'puppeteer-core/internal/cdp/Accessibility.js' ;
import {getTestState, setupTestBrowserHooks} from
'./mocha-utils.js' ;
import {attachFrame} from
'./utils.js' ;
describe(
'Accessibility' ,
function () {
setupTestBrowserHooks();
it(
'should work' , async () => {
const {page, isFirefox} = await getTestState();
await page.setContent(`
<head>
<title>Accessibility Test</title>
</head>
<body>
<div>Hello World</div>
<h1>Inputs</h1>
<input placeholder=
"Empty input" autofocus />
<input placeholder=
"readonly input" readonly />
<input placeholder=
"disabled input" disabled />
<input aria-label=
"Input with whitespace" value=
" " />
<input value=
"value only" />
<input aria-placeholder=
"placeholder" value=
"and a value" />
<div aria-hidden=
"true" id=
"desc" >
This is a description!</div>
<input aria-placeholder=
"placeholder" value=
"and a value" aria-describedby=
"desc" />
<select>
<option>First Option</option>
<option>Second Option</option>
</select>
</body>`);
await page.focus(
'[placeholder="Empty input"]' );
const golden = isFirefox
? {
role:
'document' ,
name:
'Accessibility Test' ,
children: [
{role:
'text leaf' , name:
'Hello World' },
{role:
'heading' , name:
'Inputs' , level:
1 },
{role:
'entry' , name:
'Empty input' , focused:
true },
{role:
'entry' , name:
'readonly input' , readonly:
true },
{role:
'entry' , name:
'disabled input' , disabled:
true },
{role:
'entry' , name:
'Input with whitespace' , value:
' ' },
{role:
'entry' , name:
'' , value:
'value only' },
{role:
'entry' , name:
'' , value:
'and a value' },
// firefox doesn't use aria-placeholder for the name
{
role:
'entry' ,
name:
'' ,
value:
'and a value' ,
description:
'This is a description!' ,
},
// and here
{
role:
'combobox' ,
name:
'' ,
value:
'First Option' ,
haspopup:
true ,
children: [
{
role:
'combobox option' ,
name:
'First Option' ,
selected:
true ,
},
{role:
'combobox option' , name:
'Second Option' },
],
},
],
}
: {
role:
'RootWebArea' ,
name:
'Accessibility Test' ,
children: [
{role:
'StaticText' , name:
'Hello World' },
{role:
'heading' , name:
'Inputs' , level:
1 },
{role:
'textbox' , name:
'Empty input' , focused:
true },
{role:
'textbox' , name:
'readonly input' , readonly:
true },
{role:
'textbox' , name:
'disabled input' , disabled:
true },
{role:
'textbox' , name:
'Input with whitespace' , value:
' ' },
{role:
'textbox' , name:
'' , value:
'value only' },
{role:
'textbox' , name:
'placeholder' , value:
'and a value' },
{
role:
'textbox' ,
name:
'placeholder' ,
value:
'and a value' ,
description:
'This is a description!' ,
},
{
role:
'combobox' ,
name:
'' ,
value:
'First Option' ,
haspopup:
'menu' ,
children: [
{role:
'option' , name:
'First Option' , selected:
true },
{role:
'option' , name:
'Second Option' },
],
},
],
};
expect(await page.accessibility.snapshot()).toMatchObject(golden);
});
it(
'should report uninteresting nodes' , async () => {
const {page, isFirefox} = await getTestState();
await page.setContent(`<textarea>hi</textarea>`);
await page.focus(
'textarea' );
const golden = isFirefox
? {
role:
'entry' ,
name:
'' ,
value:
'hi' ,
focused:
true ,
multiline:
true ,
children: [
{
role:
'text leaf' ,
name:
'hi' ,
},
],
}
: {
role:
'textbox' ,
name:
'' ,
value:
'hi' ,
focused:
true ,
multiline:
true ,
children: [
{
role:
'generic' ,
name:
'' ,
children: [
{
role:
'StaticText' ,
name:
'hi' ,
},
],
},
],
};
expect(
findFocusedNode(
await page.accessibility.snapshot({interestingOnly:
false }),
),
).toMatchObject(golden);
});
it(
'get snapshots while the tree is re-calculated' , async () => {
// see https://github.com/puppeteer/puppeteer/issues/9404
const {page} = await getTestState();
await page.setContent(
`<!DOCTYPE html>
<html lang=
"en" >
<head>
<meta charset=
"UTF-8" >
<meta http-equiv=
"X-UA-Compatible" content=
"IE=edge" >
<meta name=
"viewport" content=
"width=device-width, initial-scale=1.0" >
<title>Accessible name + aria-expanded puppeteer bug</title>
<style>
[aria-expanded=
"false" ] + * {
display: none;
}
</style>
</head>
<body>
<button hidden>Show</button>
<p>Some content</p>
<script>
const button = document.querySelector(
'button' );
button.removeAttribute(
'hidden' )
button.setAttribute(
'aria-expanded' ,
'false' );
button.addEventListener(
'click' ,
function () {
button.setAttribute(
'aria-expanded' , button.getAttribute(
'aria-expanded' ) !==
'true' )
if (button.getAttribute(
'aria-expanded' ) ==
'true' ) {
button.textContent =
'Hide'
}
else {
button.textContent =
'Show'
}
})
</script>
</body>
</html>`,
);
async
function getAccessibleName(page: any, element: any) {
return (await page.accessibility.snapshot({root: element})).name;
}
using button = await page.$(
'button' );
expect(await getAccessibleName(page, button)).toEqual(
'Show' );
await button?.click();
await page.waitForSelector(
'aria/Hide' );
});
it(
'roledescription' , async () => {
const {page} = await getTestState();
await page.setContent(
'<div tabIndex=-1 aria-roledescription="foo">Hi</div>' ,
);
const snapshot = await page.accessibility.snapshot();
// See https://chromium-review.googlesource.com/c/chromium/src/+/3088862
assert (snapshot);
assert (snapshot.children);
assert (snapshot.children[
0 ]);
expect(snapshot.children[
0 ]!.roledescription).toBeUndefined();
});
it(
'orientation' , async () => {
const {page} = await getTestState();
await page.setContent(
'<a href="" role="slider" aria-orientation="vertical">11</a>' ,
);
const snapshot = await page.accessibility.snapshot();
assert (snapshot);
assert (snapshot.children);
assert (snapshot.children[
0 ]);
expect(snapshot.children[
0 ]!.orientation).toEqual(
'vertical' );
});
it(
'autocomplete' , async () => {
const {page} = await getTestState();
await page.setContent(
'<input type="number" aria-autocomplete="list" />' );
const snapshot = await page.accessibility.snapshot();
assert (snapshot);
assert (snapshot.children);
assert (snapshot.children[
0 ]);
expect(snapshot.children[
0 ]!.autocomplete).toEqual(
'list' );
});
it(
'multiselectable' , async () => {
const {page} = await getTestState();
await page.setContent(
'<div role="grid" tabIndex=-1 aria-multiselectable=true>hey</div>' ,
);
const snapshot = await page.accessibility.snapshot();
assert (snapshot);
assert (snapshot.children);
assert (snapshot.children[
0 ]);
expect(snapshot.children[
0 ]!.multiselectable).toEqual(
true );
});
describe(
'iframes' , () => {
it(
'should not include iframe data if not requested' , async () => {
const {page, server} = await getTestState();
await attachFrame(page,
'frame1' , server.EMPTY_PAGE);
const frame1 = page.frames()[
1 ];
await frame1!.evaluate(() => {
const button = document.createElement(
'button' );
button.innerText =
'value1' ;
document.body.appendChild(button);
});
const snapshot = await page.accessibility.snapshot({
interestingOnly:
true ,
});
expect(snapshot).toMatchObject({
role:
'RootWebArea' ,
name:
'' ,
});
});
it(
'same-origin iframe (interesting only)' , async () => {
const {page, server} = await getTestState();
await attachFrame(page,
'frame1' , server.EMPTY_PAGE);
const frame1 = page.frames()[
1 ];
await frame1!.evaluate(() => {
const button = document.createElement(
'button' );
button.innerText =
'value1' ;
document.body.appendChild(button);
});
const snapshot = await page.accessibility.snapshot({
interestingOnly:
true ,
includeIframes:
true ,
});
expect(snapshot).toMatchObject({
role:
'RootWebArea' ,
name:
'' ,
children: [
{
role:
'Iframe' ,
name:
'' ,
children: [
{
role:
'RootWebArea' ,
name:
'' ,
children: [
{
role:
'button' ,
name:
'value1' ,
},
],
},
],
},
],
});
});
it(
'cross-origin iframe (interesting only)' , async () => {
const {page, server} = await getTestState();
await attachFrame(
page,
'frame1' ,
server.CROSS_PROCESS_PREFIX +
'/empty.html' ,
);
const frame1 = page.frames()[
1 ];
await frame1!.evaluate(() => {
const button = document.createElement(
'button' );
button.innerText =
'value1' ;
document.body.appendChild(button);
});
const snapshot = await page.accessibility.snapshot({
interestingOnly:
true ,
includeIframes:
true ,
});
expect(snapshot).toMatchObject({
role:
'RootWebArea' ,
name:
'' ,
children: [
{
role:
'Iframe' ,
name:
'' ,
children: [
{
role:
'RootWebArea' ,
name:
'' ,
children: [
{
role:
'button' ,
name:
'value1' ,
},
],
},
],
},
],
});
});
it(
'same-origin iframe (all nodes)' , async () => {
const {page, server} = await getTestState();
await attachFrame(page,
'frame1' , server.EMPTY_PAGE);
const frame1 = page.frames()[
1 ];
await frame1!.evaluate(() => {
const button = document.createElement(
'button' );
button.innerText =
'value1' ;
document.body.appendChild(button);
});
const snapshot = await page.accessibility.snapshot({
interestingOnly:
false ,
includeIframes:
true ,
});
expect(snapshot).toMatchObject({
role:
'RootWebArea' ,
name:
'' ,
children: [
{
role:
'none' ,
children: [
{
role:
'generic' ,
name:
'' ,
children: [
{
role:
'Iframe' ,
name:
'' ,
children: [
{
role:
'RootWebArea' ,
name:
'' ,
children: [
{
role:
'none' ,
children: [
{
role:
'generic' ,
name:
'' ,
children: [
{
role:
'button' ,
name:
'value1' ,
children: [
{
role:
'StaticText' ,
name:
'value1' ,
children: [
{
role:
'InlineTextBox' ,
},
],
},
],
},
],
},
],
},
],
},
],
},
],
},
],
},
],
});
});
});
it(
'keyshortcuts' , async () => {
const {page} = await getTestState();
await page.setContent(
'<div role="grid" tabIndex=-1 aria-keyshortcuts="foo">hey</div>' ,
);
const snapshot = await page.accessibility.snapshot();
assert (snapshot);
assert (snapshot.children);
assert (snapshot.children[
0 ]);
expect(snapshot.children[
0 ]!.keyshortcuts).toEqual(
'foo' );
});
describe(
'filtering children of leaf nodes' ,
function () {
it(
'should not report text nodes inside controls' , async () => {
const {page, isFirefox} = await getTestState();
await page.setContent(`
<div role=
"tablist" >
<div role=
"tab" aria-selected=
"true" ><b>Tab1</b></div>
<div role=
"tab" >Tab2</div>
</div>`);
const golden = isFirefox
? {
role:
'document' ,
name:
'' ,
children: [
{
role:
'pagetab' ,
name:
'Tab1' ,
selected:
true ,
},
{
role:
'pagetab' ,
name:
'Tab2' ,
},
],
}
: {
role:
'RootWebArea' ,
name:
'' ,
children: [
{
role:
'tab' ,
name:
'Tab1' ,
selected:
true ,
},
{
role:
'tab' ,
name:
'Tab2' ,
},
],
};
expect(await page.accessibility.snapshot()).toMatchObject(golden);
});
it(
'rich text editable fields should have children' , async () => {
const {page, isFirefox} = await getTestState();
await page.setContent(`
<div contenteditable=
"true" >
Edit
this image: <img src=
"fakeimage.png" alt=
"my fake image" >
</div>`);
const golden = isFirefox
? {
role:
'section' ,
name:
'' ,
children: [
{
role:
'text leaf' ,
name:
'Edit this image:' ,
},
{
role:
'StaticText' ,
name:
'my fake image' ,
},
],
}
: {
role:
'generic' ,
name:
'' ,
value:
'Edit this image: ' ,
children: [
{
role:
'StaticText' ,
name:
'Edit this image: ' ,
},
{
role:
'image' ,
name:
'my fake image' ,
},
],
};
const snapshot = await page.accessibility.snapshot();
assert (snapshot);
assert (snapshot.children);
expect(snapshot.children[
0 ]).toMatchObject(golden);
});
it(
'rich text editable fields with role should have children' , async () => {
const {page, isFirefox} = await getTestState();
await page.setContent(`
<div contenteditable=
"true" role=
'textbox' >
Edit
this image: <img src=
"fakeimage.png" alt=
"my fake image" >
</div>`);
// Image node should not be exposed in contenteditable elements. See https://crbug.com/1324392.
const golden = isFirefox
? {
role:
'entry' ,
name:
'' ,
value:
'Edit this image: my fake image' ,
children: [
{
role:
'StaticText' ,
name:
'my fake image' ,
},
],
}
: {
role:
'textbox' ,
name:
'' ,
value:
'Edit this image: ' ,
multiline:
true ,
children: [
{
role:
'StaticText' ,
name:
'Edit this image: ' ,
},
],
};
const snapshot = await page.accessibility.snapshot();
assert (snapshot);
assert (snapshot.children);
expect(snapshot.children[
0 ]).toMatchObject(golden);
});
// Firefox does not support contenteditable="plaintext-only".
describe(
'plaintext contenteditable' ,
function () {
it(
'plain text field with role should not have children' , async () => {
const {page} = await getTestState();
await page.setContent(`
<div contenteditable=
"plaintext-only" role=
'textbox' >Edit
this image:<img src=
"fakeimage.png" alt=
"my fake image" ></div>`);
const snapshot = await page.accessibility.snapshot();
assert (snapshot);
assert (snapshot.children);
expect(snapshot.children[
0 ]).toMatchObject({
role:
'textbox' ,
name:
'' ,
value:
'Edit this image:' ,
multiline:
true ,
});
});
});
it(
'non editable textbox with role and tabIndex and label should not have children' , async () =>
{
const {page, isFirefox} = await getTestState();
await page.setContent(`
<div role="textbox" tabIndex=0 aria-checked="true" aria-label="my favorite textbox" >
this is the inner content
<img alt="yo" src="fakeimg.png" >
</div>`);
const golden = isFirefox
? {
role: 'entry' ,
name: 'my favorite textbox' ,
value: 'this is the inner content yo' ,
}
: {
role: 'textbox' ,
name: 'my favorite textbox' ,
value: 'this is the inner content ' ,
};
const snapshot = await page.accessibility.snapshot();
assert (snapshot);
assert (snapshot.children);
expect(snapshot.children[0 ]).toMatchObject(golden);
});
it('checkbox with and tabIndex and label should not have children' , async () => {
const {page, isFirefox} = await getTestState();
await page.setContent(`
<div role="checkbox" tabIndex=0 aria-checked="true" aria-label="my favorite checkbox" >
this is the inner content
<img alt="yo" src="fakeimg.png" >
</div>`);
const golden = isFirefox
? {
role: 'checkbutton' ,
name: 'my favorite checkbox' ,
checked: true ,
}
: {
role: 'checkbox' ,
name: 'my favorite checkbox' ,
checked: true ,
};
const snapshot = await page.accessibility.snapshot();
assert (snapshot);
assert (snapshot.children);
expect(snapshot.children[0 ]).toMatchObject(golden);
});
it('checkbox without label should not have children' , async () => {
const {page, isFirefox} = await getTestState();
await page.setContent(`
<div role="checkbox" aria-checked="true" >
this is the inner content
<img alt="yo" src="fakeimg.png" >
</div>`);
const golden = isFirefox
? {
role: 'checkbutton' ,
name: 'this is the inner content yo' ,
checked: true ,
}
: {
role: 'checkbox' ,
name: 'this is the inner content yo' ,
checked: true ,
};
const snapshot = await page.accessibility.snapshot();
assert (snapshot);
assert (snapshot.children);
expect(snapshot.children[0 ]).toMatchObject(golden);
});
describe('root option' , function () {
it('should work a button' , async () => {
const {page} = await getTestState();
await page.setContent(`<button>My Button</button>`);
using button = (await page.$('button' ))!;
expect(await page.accessibility.snapshot({root: button})).toMatchObject(
{
role: 'button' ,
name: 'My Button' ,
},
);
});
it('should work an input' , async () => {
const {page} = await getTestState();
await page.setContent(`<input title="My Input" value="My Value" >`);
using input = (await page.$('input' ))!;
expect(await page.accessibility.snapshot({root: input})).toMatchObject({
role: 'textbox' ,
name: 'My Input' ,
value: 'My Value' ,
});
});
it('should work a menu' , async () => {
const {page} = await getTestState();
await page.setContent(`
<div role="menu" title="My Menu" >
<div role="menuitem" >First Item</div>
<div role="menuitem" >Second Item</div>
<div role="menuitem" >Third Item</div>
</div>
`);
using menu = (await page.$('div[role="menu"]' ))!;
expect(await page.accessibility.snapshot({root: menu})).toMatchObject({
role: 'menu' ,
name: 'My Menu' ,
children: [
{role: 'menuitem' , name: 'First Item' },
{role: 'menuitem' , name: 'Second Item' },
{role: 'menuitem' , name: 'Third Item' },
],
orientation: 'vertical' ,
});
});
it('should return null when the element is no longer in DOM' , async () => {
const {page} = await getTestState();
await page.setContent(`<button>My Button</button>`);
using button = (await page.$('button' ))!;
await page.$eval('button' , button => {
return button.remove();
});
expect(await page.accessibility.snapshot({root: button})).toEqual(null );
});
it('should support the interestingOnly option' , async () => {
const {page} = await getTestState();
await page.setContent(`<div><button>My Button</button></div>`);
using div = (await page.$('div' ))!;
expect(await page.accessibility.snapshot({root: div})).toEqual(null );
expect(
await page.accessibility.snapshot({
root: div,
interestingOnly: false ,
}),
).toMatchObject({
role: 'generic' ,
name: '' ,
children: [
{
role: 'button' ,
name: 'My Button' ,
children: [{role: 'StaticText' , name: 'My Button' }],
},
],
});
});
});
describe('elementHandle()' , () => {
it('should get an ElementHandle from a snapshot item' , async () => {
const {page} = await getTestState();
await page.setContent(`<button>My Button</button>`);
using button = (await page.$('button' ))!;
const snapshot = await page.accessibility.snapshot({root: button});
expect(snapshot).toMatchObject({
role: 'button' ,
name: 'My Button' ,
});
using buttonHandle = await snapshot!.elementHandle();
expect(
await buttonHandle?.evaluate(button => {
return button.innerHTML;
}),
).toEqual('My Button' );
});
});
});
function findFocusedNode(
node: SerializedAXNode | null ,
): SerializedAXNode | null {
if (node?.focused) {
return node;
}
for (const child of node?.children || []) {
const focusedChild = findFocusedNode(child);
if (focusedChild) {
return focusedChild;
}
}
return null ;
}
});
Messung V0.5 in Prozent C=98 H=87 G=92
¤ Dauer der Verarbeitung: 0.11 Sekunden
(vorverarbeitet am 2026-06-04)
¤
*© Formatika GbR, Deutschland