/**
* Copyright 2020 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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 find button', async () => {
const {page} = await getTestState();
await page.setContent(
' Submit button and some spaces '
);
const expectFound = async (button: ElementHandle | null) => {
assert(button);
const id = await button.evaluate((button: Element) => {
return button.id;
});
expect(id).toBe('btn');
};
let button = await page.$(
'aria/Submit button and some spaces[role="button"]'
);
await expectFound(button);
button = await page.$(
"aria/Submit button and some spaces[role='button']"
);
await expectFound(button);
button = await page.$(
'aria/ Submit button and some spaces[role="button"]'
);
await expectFound(button);
button = await page.$(
'aria/Submit button and some spaces [role="button"]'
);
await expectFound(button);
button = await page.$(
'aria/Submit button and some spaces [ role = "button" ] '
);
await expectFound(button);
button = await page.$(
'aria/[role="button"]Submit button and some spaces'
);
await expectFound(button);
button = await page.$(
'aria/Submit button [role="button"]and some spaces'
);
await expectFound(button);
button = await page.$(
'aria/[name=" Submit button and some spaces"][role="button"]'
);
await expectFound(button);
button = await page.$(
"aria/[name=' Submit button and some spaces'][role='button']"
);
await expectFound(button);
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(
'
Submit
'
);
const button = (await page.$(
'aria/[role="button"]'
)) as ElementHandle;
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(
'Submit
'
);
const button = (await page.$(
'aria/Submit[role="button"]'
)) as ElementHandle;
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(
`
`
);
const div = (await page.$(
'aria/menu div'
)) as ElementHandle;
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(
`
menu div
menu div
`
);
const menu = (await page.$(
'aria/menu-label1'
)) as ElementHandle;
const id = await menu!.evaluate(div => {
return div.id;
});
expect(id).toBe('mnu1');
});
it('should find by name', async () => {
const {page} = await getTestState();
await page.setContent(
`
menu div
menu div
`
);
const menu = (await page.$(
'aria/menu-label2'
)) as ElementHandle;
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(
`
`
);
const divs = (await page.$$('aria/menu div')) as Array<
ElementHandle
>;
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(25_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 = `test
`);
});
const 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(`anything `),
]);
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');
const 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 =
'
');
});
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');
const 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');
const 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).toContain(
'waitForFunction failed: frame got detached.'
);
});
it('should survive cross-process navigation', async () => {
const {page, server} = await getTestState();
let imgFound = false;
const waitForSelector = page
.waitForSelector('aria/[role="img"]')
.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(
`1
`
);
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(
``
);
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(
`text
`
);
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(
`text
`
);
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(`text
`);
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();
const 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(`text
`);
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(`
`);
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(`anything
`);
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 {
const state = await getTestState();
await state.page.setContent(
`
title
title
text content
text content
text content
Accessible Name
`
);
return state;
}
const getIds = async (elements: ElementHandle[]) => {
return Promise.all(
elements.map(element => {
return element.evaluate((element: 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
>;
const ids = await getIds(found);
expect(ids).toEqual([
'node5',
'node6',
'node7',
'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', 'hidden', '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']);
});
});
});