/**
* @license
* Copyright 2020 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import assert from 'assert' ;
import expect from 'expect' ;
import {TimeoutError} from 'puppeteer' ;
import type {ElementHandle} from 'puppeteer-core/internal/api/ElementHandle.js' ;
import {getTestState, setupTestBrowserHooks} from './mocha-utils.js' ;
import {attachFrame, detachFrame} from './utils.js' ;
describe('AriaQueryHandler' , () => {
setupTestBrowserHooks();
describe('parseAriaSelector' , () => {
it('should handle non-breaking spaces' , async () => {
const {page} = await getTestState();
await page.setContent(
'<button id="btn" role="button"><span> </span><span> </span>Submit button and some spaces</button>' ,
);
const expectFound = async (button: ElementHandle | null ) => {
assert (button);
const id = await button.evaluate(button => {
return button.id;
});
expect(id).toBe('btn' );
};
{
using button = await page.$(
'aria/\u00A0\u00A0Submit button and some spaces' ,
);
await expectFound(button);
}
{
using button = await page.$('aria/Submit button and some spaces' );
expect(button).toBe(null );
}
});
it('should handle non-breaking spaces' , async () => {
const {page} = await getTestState();
await page.setContent(
'<button id="btn" role="button"> Submit button and some spaces</button>' ,
);
const expectFound = async (button: ElementHandle | null ) => {
assert (button);
const id = await button.evaluate(button => {
return button.id;
});
expect(id).toBe('btn' );
};
{
using button = await page.$('aria/ubmit button and some spaces' );
expect(button).toBe(null );
}
{
using button = await page.$('aria/Submit button and some spaces' );
await expectFound(button);
}
});
it('should handle zero width spaces' , async () => {
const {page} = await getTestState();
await page.setContent(
'<button id="btn" role="button"><span>​</span><span>​</span>Submit button and some spaces</button>' ,
);
const expectFound = async (button: ElementHandle | null ) => {
assert (button);
const id = await button.evaluate(button => {
return button.id;
});
expect(id).toBe('btn' );
};
{
using button = await page.$(
'aria/\u200B\u200BSubmit button and some spaces' ,
);
await expectFound(button);
}
{
using button = await page.$('aria/Submit button and some spaces' );
expect(button).toBe(null );
}
});
it('should find button' , async () => {
const {page} = await getTestState();
await page.setContent(
'<button id="btn" role="button"> Submit button and some spaces </button>' ,
);
const expectFound = async (button: ElementHandle | null ) => {
assert (button);
const id = await button.evaluate(button => {
return button.id;
});
expect(id).toBe('btn' );
};
{
using button = await page.$(
'aria/Submit button and some spaces[role="button"]' ,
);
await expectFound(button);
}
{
using button = await page.$(
"aria/Submit button and some spaces[role='button']" ,
);
await expectFound(button);
}
{
using button = await page.$(
'aria/ Submit button and some spaces[role="button"]' ,
);
expect(button).toBe(null );
}
{
using button = await page.$(
'aria/Submit button and some spaces [role="button"]' ,
);
expect(button).toBe(null );
}
{
using button = await page.$(
'aria/Submit button and some spaces [ role = "button" ] ' ,
);
expect(button).toBe(null );
}
{
using button = await page.$(
'aria/[role="button"]Submit button and some spaces' ,
);
await expectFound(button);
}
{
using button = await page.$(
'aria/Submit button [role="button"]and some spaces' ,
);
await expectFound(button);
}
{
using button = await page.$(
'aria/[name=" Submit button and some spaces"][role="button"]' ,
);
expect(button).toBe(null );
}
{
using button = await page.$(
"aria/[name=' Submit button and some spaces'][role='button']" ,
);
expect(button).toBe(null );
}
{
using button = await page.$(
'aria/ignored[name="Submit button and some spaces"][role="button"]' ,
);
await expectFound(button);
await expect(page.$('aria/smth[smth="true"]' )).rejects.toThrow(
'Unknown aria attribute "smth" in selector' ,
);
}
});
});
describe('queryOne' , () => {
it('should find button by role' , async () => {
const {page} = await getTestState();
await page.setContent(
'<div id="div"><button id="btn" role="button">Submit</button></div>' ,
);
using button = (await page.$(
'aria/[role="button"]' ,
)) as ElementHandle<HTMLButtonElement>;
const id = await button!.evaluate(button => {
return button.id;
});
expect(id).toBe('btn' );
});
it('should find button by name and role' , async () => {
const {page} = await getTestState();
await page.setContent(
'<div id="div"><button id="btn" role="button">Submit</button></div>' ,
);
using button = (await page.$(
'aria/Submit[role="button"]' ,
)) as ElementHandle<HTMLButtonElement>;
const id = await button!.evaluate(button => {
return button.id;
});
expect(id).toBe('btn' );
});
it('should find first matching element' , async () => {
const {page} = await getTestState();
await page.setContent(
`
<div role="menu" id="mnu1" aria-label="menu div" ></div>
<div role="menu" id="mnu2" aria-label="menu div" ></div>
`,
);
using div = (await page.$(
'aria/menu div' ,
)) as ElementHandle<HTMLDivElement>;
const id = await div!.evaluate(div => {
return div.id;
});
expect(id).toBe('mnu1' );
});
it('should find by name' , async () => {
const {page} = await getTestState();
await page.setContent(
`
<div role="menu" id="mnu1" aria-label="menu-label1" >menu div</div>
<div role="menu" id="mnu2" aria-label="menu-label2" >menu div</div>
`,
);
using menu = (await page.$(
'aria/menu-label1' ,
)) as ElementHandle<HTMLDivElement>;
const id = await menu!.evaluate(div => {
return div.id;
});
expect(id).toBe('mnu1' );
});
it('should find 2nd element by name' , async () => {
const {page} = await getTestState();
await page.setContent(
`
<div role="menu" id="mnu1" aria-label="menu-label1" >menu div</div>
<div role="menu" id="mnu2" aria-label="menu-label2" >menu div</div>
`,
);
using menu = (await page.$(
'aria/menu-label2' ,
)) as ElementHandle<HTMLDivElement>;
const id = await menu!.evaluate(div => {
return div.id;
});
expect(id).toBe('mnu2' );
});
});
describe('queryAll' , () => {
it('should find menu by name' , async () => {
const {page} = await getTestState();
await page.setContent(
`
<div role="menu" id="mnu1" aria-label="menu div" ></div>
<div role="menu" id="mnu2" aria-label="menu div" ></div>
`,
);
const divs = (await page.$$('aria/menu div' )) as Array<
ElementHandle<HTMLDivElement>
>;
const ids = await Promise.all(
divs.map(n => {
return n.evaluate(div => {
return div.id;
});
}),
);
expect(ids.join(', ' )).toBe('mnu1, mnu2' );
});
});
describe('queryAllArray' , () => {
it('$$eval should handle many elements' , async function () {
this .timeout(40 _000 );
const {page} = await getTestState();
await page.setContent('' );
await page.evaluate(
`
for (var i = 0 ; i <= 10000 ; i++) {
const button = document.createElement('button' );
button.textContent = i;
document.body.appendChild(button);
}
`,
);
const sum = await page.$$eval('aria/[role="button"]' , buttons => {
return buttons.reduce((acc, button) => {
return acc + Number(button.textContent);
}, 0 );
});
expect(sum).toBe(50005000 );
});
});
describe('waitForSelector (aria)' , function () {
const addElement = (tag: string) => {
return document.body.appendChild(document.createElement(tag));
};
it('should immediately resolve promise if node exists' , async () => {
const {page, server} = await getTestState();
await page.goto (server.EMPTY_PAGE);
await page.evaluate(addElement, 'button' );
await page.waitForSelector('aria/[role="button"]' );
});
it('should work for ElementHandle.waitForSelector' , async () => {
const {page, server} = await getTestState();
await page.goto (server.EMPTY_PAGE);
await page.evaluate(() => {
return (document.body.innerHTML = `<div><button>test</button></div>`);
});
using element = (await page.$('div' ))!;
await element!.waitForSelector('aria/test' );
});
it('should persist query handler bindings across reloads' , async () => {
const {page, server} = await getTestState();
await page.goto (server.EMPTY_PAGE);
await page.evaluate(addElement, 'button' );
await page.waitForSelector('aria/[role="button"]' );
await page.reload();
await page.evaluate(addElement, 'button' );
await page.waitForSelector('aria/[role="button"]' );
});
it('should persist query handler bindings across navigations' , async () => {
const {page, server} = await getTestState();
// Reset page but make sure that execution context ids start with 1.
await page.goto ('data:text/html,' );
await page.goto (server.EMPTY_PAGE);
await page.evaluate(addElement, 'button' );
await page.waitForSelector('aria/[role="button"]' );
// Reset page but again make sure that execution context ids start with 1.
await page.goto ('data:text/html,' );
await page.goto (server.EMPTY_PAGE);
await page.evaluate(addElement, 'button' );
await page.waitForSelector('aria/[role="button"]' );
});
it('should work independently of `exposeFunction`' , async () => {
const {page, server} = await getTestState();
await page.goto (server.EMPTY_PAGE);
await page.exposeFunction('ariaQuerySelector' , (a: number, b: number) => {
return a + b;
});
await page.evaluate(addElement, 'button' );
await page.waitForSelector('aria/[role="button"]' );
const result = await page.evaluate('globalThis.ariaQuerySelector(2,8)' );
expect(result).toBe(10 );
});
it('should work with removed MutationObserver' , async () => {
const {page} = await getTestState();
await page.evaluate(() => {
// @ts-expect-error This is the point of the test.
return delete window.MutationObserver;
});
const [handle] = await Promise.all([
page.waitForSelector('aria/anything' ),
page.setContent(`<h1>anything</h1>`),
]);
assert (handle);
expect(
await page.evaluate(x => {
return x.textContent;
}, handle),
).toBe('anything' );
});
it('should resolve promise when node is added' , async () => {
const {page, server} = await getTestState();
await page.goto (server.EMPTY_PAGE);
const frame = page.mainFrame();
const watchdog = frame.waitForSelector('aria/[role="heading"]' );
await frame.evaluate(addElement, 'br' );
await frame.evaluate(addElement, 'h1' );
using elementHandle = (await watchdog)!;
const tagName = await (
await elementHandle.getProperty('tagName' )
).jsonValue();
expect(tagName).toBe('H1' );
});
it('should work when node is added through innerHTML' , async () => {
const {page, server} = await getTestState();
await page.goto (server.EMPTY_PAGE);
const watchdog = page.waitForSelector('aria/name' );
await page.evaluate(addElement, 'span' );
await page.evaluate(() => {
return (document.querySelector('span' )!.innerHTML =
'<h3><div aria-label="name"></div></h3>' );
});
await watchdog;
});
it('Page.waitForSelector is shortcut for main frame' , async () => {
const {page, server} = await getTestState();
await page.goto (server.EMPTY_PAGE);
await attachFrame(page, 'frame1' , server.EMPTY_PAGE);
const otherFrame = page.frames()[1 ];
const watchdog = page.waitForSelector('aria/[role="button"]' );
await otherFrame!.evaluate(addElement, 'button' );
await page.evaluate(addElement, 'button' );
using elementHandle = await watchdog;
expect(elementHandle!.frame).toBe(page.mainFrame());
});
it('should run in specified frame' , async () => {
const {page, server} = await getTestState();
await attachFrame(page, 'frame1' , server.EMPTY_PAGE);
await attachFrame(page, 'frame2' , server.EMPTY_PAGE);
const frame1 = page.frames()[1 ];
const frame2 = page.frames()[2 ];
const waitForSelectorPromise = frame2!.waitForSelector(
'aria/[role="button"]' ,
);
await frame1!.evaluate(addElement, 'button' );
await frame2!.evaluate(addElement, 'button' );
using elementHandle = await waitForSelectorPromise;
expect(elementHandle!.frame).toBe(frame2);
});
it('should throw when frame is detached' , async () => {
const {page, server} = await getTestState();
await attachFrame(page, 'frame1' , server.EMPTY_PAGE);
const frame = page.frames()[1 ];
let waitError!: Error;
const waitPromise = frame!
.waitForSelector('aria/does-not-exist' )
.catch (error => {
return (waitError = error);
});
await detachFrame(page, 'frame1' );
await waitPromise;
expect(waitError).toBeTruthy();
expect(waitError.message).atLeastOneToContain([
'waitForFunction failed: frame got detached.' ,
'Browsing context already closed.' ,
]);
});
it('should survive cross-process navigation' , async () => {
const {page, server} = await getTestState();
let imgFound = false ;
const waitForSelector = page
.waitForSelector('aria/[role="image"]' )
.then(() => {
return (imgFound = true );
});
await page.goto (server.EMPTY_PAGE);
expect(imgFound).toBe(false );
await page.reload();
expect(imgFound).toBe(false );
await page.goto (server.CROSS_PROCESS_PREFIX + '/grid.html' );
await waitForSelector;
expect(imgFound).toBe(true );
});
it('should wait for visible' , async () => {
const {page} = await getTestState();
let divFound = false ;
const waitForSelector = page
.waitForSelector('aria/name' , {visible: true })
.then(() => {
return (divFound = true );
});
await page.setContent(
`<div aria-label='name' style='display: none; visibility: hidden;' >1 </div>`,
);
expect(divFound).toBe(false );
await page.evaluate(() => {
return document.querySelector('div' )!.style.removeProperty('display' );
});
expect(divFound).toBe(false );
await page.evaluate(() => {
return document
.querySelector('div' )!
.style.removeProperty('visibility' );
});
expect(await waitForSelector).toBe(true );
expect(divFound).toBe(true );
});
it('should wait for visible recursively' , async () => {
const {page} = await getTestState();
let divVisible = false ;
const waitForSelector = page
.waitForSelector('aria/inner' , {visible: true })
.then(() => {
return (divVisible = true );
})
.catch (() => {
return (divVisible = false );
});
await page.setContent(
`<div style='display: none; visibility: hidden;' ><div aria-label="inner" >hi</div></div>`,
);
expect(divVisible).toBe(false );
await page.evaluate(() => {
return document.querySelector('div' )!.style.removeProperty('display' );
});
expect(divVisible).toBe(false );
await page.evaluate(() => {
return document
.querySelector('div' )!
.style.removeProperty('visibility' );
});
expect(await waitForSelector).toBe(true );
expect(divVisible).toBe(true );
});
it('hidden should wait for visibility: hidden' , async () => {
const {page} = await getTestState();
let divHidden = false ;
await page.setContent(
`<div role='button' style='display: block;' >text</div>`,
);
const waitForSelector = page
.waitForSelector('aria/[role="button"]' , {hidden: true })
.then(() => {
return (divHidden = true );
})
.catch (() => {
return (divHidden = false );
});
await page.waitForSelector('aria/[role="button"]' ); // do a round trip
expect(divHidden).toBe(false );
await page.evaluate(() => {
return document
.querySelector('div' )!
.style.setProperty('visibility' , 'hidden' );
});
expect(await waitForSelector).toBe(true );
expect(divHidden).toBe(true );
});
it('hidden should wait for display: none' , async () => {
const {page} = await getTestState();
let divHidden = false ;
await page.setContent(
`<div role='main' style='display: block;' >text</div>`,
);
const waitForSelector = page
.waitForSelector('aria/[role="main"]' , {hidden: true })
.then(() => {
return (divHidden = true );
})
.catch (() => {
return (divHidden = false );
});
await page.waitForSelector('aria/[role="main"]' ); // do a round trip
expect(divHidden).toBe(false );
await page.evaluate(() => {
return document
.querySelector('div' )!
.style.setProperty('display' , 'none' );
});
expect(await waitForSelector).toBe(true );
expect(divHidden).toBe(true );
});
it('hidden should wait for removal' , async () => {
const {page} = await getTestState();
await page.setContent(`<div role='main' >text</div>`);
let divRemoved = false ;
const waitForSelector = page
.waitForSelector('aria/[role="main"]' , {hidden: true })
.then(() => {
return (divRemoved = true );
})
.catch (() => {
return (divRemoved = false );
});
await page.waitForSelector('aria/[role="main"]' ); // do a round trip
expect(divRemoved).toBe(false );
await page.evaluate(() => {
return document.querySelector('div' )!.remove();
});
expect(await waitForSelector).toBe(true );
expect(divRemoved).toBe(true );
});
it('should return null if waiting to hide non-existing element' , async () => {
const {page} = await getTestState();
using handle = await page.waitForSelector('aria/non-existing' , {
hidden: true ,
});
expect(handle).toBe(null );
});
it('should respect timeout' , async () => {
const {page} = await getTestState();
const error = await page
.waitForSelector('aria/[role="button"]' , {
timeout: 10 ,
})
.catch (error => {
return error;
});
expect(error.message).toContain(
'Waiting for selector `[role="button"]` failed: Waiting failed: 10ms exceeded' ,
);
expect(error).toBeInstanceOf(TimeoutError);
});
it('should have an error message specifically for awaiting an element to be hidden' , async () => {
const {page} = await getTestState();
await page.setContent(`<div role='main' >text</div>`);
const promise = page.waitForSelector('aria/[role="main"]' , {
hidden: true ,
timeout: 10 ,
});
await expect(promise).rejects.toMatchObject({
message:
'Waiting for selector `[role="main"]` failed: Waiting failed: 10ms exceeded' ,
});
});
it('should respond to node attribute mutation' , async () => {
const {page} = await getTestState();
let divFound = false ;
const waitForSelector = page
.waitForSelector('aria/zombo' )
.then(() => {
return (divFound = true );
})
.catch (() => {
return (divFound = false );
});
await page.setContent(`<div aria-label='notZombo' ></div>`);
expect(divFound).toBe(false );
await page.evaluate(() => {
return document
.querySelector('div' )!
.setAttribute('aria-label' , 'zombo' );
});
expect(await waitForSelector).toBe(true );
});
it('should return the element handle' , async () => {
const {page} = await getTestState();
const waitForSelector = page.waitForSelector('aria/zombo' ).catch (err => {
return err;
});
await page.setContent(`<div aria-label='zombo' >anything</div>`);
expect(
await page.evaluate(
x => {
return x?.textContent;
},
await waitForSelector,
),
).toBe('anything' );
});
it('should have correct stack trace for timeout' , async () => {
const {page} = await getTestState();
let error!: Error;
await page.waitForSelector('aria/zombo' , {timeout: 10 }).catch (error_ => {
return (error = error_);
});
expect(error!.stack).toContain(
'Waiting for selector `zombo` failed: Waiting failed: 10ms exceeded' ,
);
});
});
describe('queryOne (Chromium web test)' , () => {
async function setupPage(): ReturnType<typeof getTestState> {
const state = await getTestState();
await state.page.setContent(
`
<h2 id="shown" >title</h2>
<h2 id="hidden" aria-hidden="true" >title</h2>
<div id="node1" aria-labelledby="node2" ></div>
<div id="node2" aria-label="bar" ></div>
<div id="node3" aria-label="foo" ></div>
<div id="node4" class ="container" >
<div id="node5" role="button" aria-label="foo" ></div>
<div id="node6" role="button" aria-label="foo" ></div>
<!-- Accessible name not available when element is hidden -->
<div id="node7" hidden role="button" aria-label="foo" ></div>
<div id="node8" role="button" aria-label="bar" ></div>
</div>
<button id="node10" >text content</button>
<h1 id="node11" >text content</h1>
<!-- Accessible name not available when role is "presentation" -->
<h1 id="node12" role="presentation" >text content</h1>
<!-- Elements inside shadow dom should be found -->
<script>
const div = document.createElement('div' );
const shadowRoot = div.attachShadow({mode: 'open' });
const h1 = document.createElement('h1' );
h1.textContent = 'text content' ;
h1.id = 'node13' ;
shadowRoot.appendChild(h1);
document.documentElement.appendChild(div);
</script>
<img id="node20" src="" alt="Accessible Name" >
<input id="node21" type="submit" value="Accessible Name" >
<label id="node22" for ="node23" >Accessible Name</label>
<!-- Accessible name for the <input> is "Accessible Name" -->
<input id="node23" >
<div id="node24" title="Accessible Name" ></div>
<div role="tree" >
<div role="treeitem" id="node30" >
<div role="treeitem" id="node31" >
<div role="treeitem" id="node32" >item1</div>
<div role="treeitem" id="node33" >item2</div>
</div>
<div role="treeitem" id="node34" >item3</div>
</div>
</div>
<!-- Accessible name for the <div> is "item1 item2 item3" -->
<div aria-describedby="node30" ></div>
`,
);
return state;
}
const getIds = async (elements: ElementHandle[]) => {
return await Promise.all(
elements.map(element => {
return element.evaluate(element => {
return element.id;
});
}),
);
};
it('should find by name "foo"' , async () => {
const {page} = await setupPage();
const found = await page.$$('aria/foo' );
const ids = await getIds(found);
expect(ids).toEqual(['node3' , 'node5' , 'node6' ]);
});
it('should find by name "bar"' , async () => {
const {page} = await setupPage();
const found = await page.$$('aria/bar' );
const ids = await getIds(found);
expect(ids).toEqual(['node1' , 'node2' , 'node8' ]);
});
it('should find treeitem by name' , async () => {
const {page} = await setupPage();
const found = await page.$$('aria/item1 item2 item3' );
const ids = await getIds(found);
expect(ids).toEqual(['node30' ]);
});
it('should find by role "button"' , async () => {
const {page} = await setupPage();
const found = (await page.$$('aria/[role="button"]' )) as Array<
ElementHandle<HTMLButtonElement>
>;
const ids = await getIds(found);
expect(ids).toEqual(['node5' , 'node6' , 'node8' , 'node10' , 'node21' ]);
});
it('should find by role "heading"' , async () => {
const {page} = await setupPage();
const found = await page.$$('aria/[role="heading"]' );
const ids = await getIds(found);
expect(ids).toEqual(['shown' , 'node11' , 'node13' ]);
});
it('should find both ignored and unignored' , async () => {
const {page} = await setupPage();
const found = await page.$$('aria/title' );
const ids = await getIds(found);
expect(ids).toEqual(['shown' ]);
});
});
});
Messung V0.5 in Prozent C=98 H=87 G=92
¤ Dauer der Verarbeitung: 0.15 Sekunden
(vorverarbeitet am 2026-06-04)
¤
*© Formatika GbR, Deutschland