feat(a11y-query): introduce internal handlers (#6437)

This commit changes the internal representation of query handlers to contain Puppeteer-level code instead of page functions.
The interface `CustomQueryHandler` is introduced for user-defined query handlers. When a `CustomQueryHandler` is registered using  `registerCustomQueryHandler` a corresponding Puppeteer-level handler is created through `makeQueryHandler` by wrapping the page functions as appropriate.
The internal query handlers (defined by the interface `QueryHandler`) contain two new functions: `waitFor` and `queryAllArray`.
- `waitFor` allows page-based handlers to make use of the `WaitTask`-backed implementation in `DOMWorld`, whereas purely Puppeteer-based handlers can define an alternative approach instead.
- `queryAllArray` is similar to `queryAll` but with a slightly different interface; it returns a `JSHandle` to an array with the results as opposed to an array of `ElementHandle`. It is used by `$$eval`. 

After this change, we can introduce built-in query handlers that are not executed in the page context (#6307).
This commit is contained in:
Johan Bay
2020-09-23 16:02:22 +02:00
committed by GitHub
parent 1396c9d4cd
commit 72fe86fe6a
5 changed files with 266 additions and 62 deletions

View File

@@ -19,6 +19,7 @@ import {
setupTestBrowserHooks,
setupTestPageAndContextHooks,
} from './mocha-utils'; // eslint-disable-line import/extensions
import { CustomQueryHandler } from '../lib/cjs/puppeteer/common/QueryHandler.js';
describe('querySelector', function () {
setupTestBrowserHooks();
@@ -67,6 +68,9 @@ describe('querySelector', function () {
});
});
// The tests for $$eval are repeated later in this file in the test group 'QueryAll'.
// This is done to also test a query handler where QueryAll returns an Element[]
// as opposed to NodeListOf<Element>.
describe('Page.$$eval', function () {
it('should work', async () => {
const { page } = getTestState();
@@ -77,6 +81,52 @@ describe('querySelector', function () {
const divsCount = await page.$$eval('div', (divs) => divs.length);
expect(divsCount).toBe(3);
});
it('should accept extra arguments', async () => {
const { page } = getTestState();
await page.setContent(
'<div>hello</div><div>beautiful</div><div>world!</div>'
);
const divsCountPlus5 = await page.$$eval(
'div',
(divs, two: number, three: number) => divs.length + two + three,
2,
3
);
expect(divsCountPlus5).toBe(8);
});
it('should accept ElementHandles as arguments', async () => {
const { page } = getTestState();
await page.setContent(
'<section>2</section><section>2</section><section>1</section><div>3</div>'
);
const divHandle = await page.$('div');
const sum = await page.$$eval(
'section',
(sections, div: HTMLElement) =>
sections.reduce(
(acc, section) => acc + Number(section.textContent),
0
) + Number(div.textContent),
divHandle
);
expect(sum).toBe(8);
});
it('should handle many elements', async () => {
const { page } = getTestState();
await page.evaluate(
`
for (var i = 0; i <= 1000; i++) {
const section = document.createElement('section');
section.textContent = i;
document.body.appendChild(section);
}
`
);
const sum = await page.$$eval('section', (sections) =>
sections.reduce((acc, section) => acc + Number(section.textContent), 0)
);
expect(sum).toBe(500500);
});
});
describe('Page.$', function () {
@@ -312,4 +362,100 @@ describe('querySelector', function () {
expect(second).toEqual([]);
});
});
// This is the same tests for `$$eval` and `$$` as above, but with a queryAll
// handler that returns an array instead of a list of nodes.
describe('QueryAll', function () {
const handler: CustomQueryHandler = {
queryAll: (element: Element, selector: string) =>
Array.from(element.querySelectorAll(selector)),
};
before(() => {
const { puppeteer } = getTestState();
puppeteer.__experimental_registerCustomQueryHandler('allArray', handler);
});
it('$$ should query existing elements', async () => {
const { page } = getTestState();
await page.setContent(
'<html><body><div>A</div><br/><div>B</div></body></html>'
);
const html = await page.$('html');
const elements = await html.$$('allArray/div');
expect(elements.length).toBe(2);
const promises = elements.map((element) =>
page.evaluate((e: HTMLElement) => e.textContent, element)
);
expect(await Promise.all(promises)).toEqual(['A', 'B']);
});
it('$$ should return empty array for non-existing elements', async () => {
const { page } = getTestState();
await page.setContent(
'<html><body><span>A</span><br/><span>B</span></body></html>'
);
const html = await page.$('html');
const elements = await html.$$('allArray/div');
expect(elements.length).toBe(0);
});
it('$$eval should work', async () => {
const { page } = getTestState();
await page.setContent(
'<div>hello</div><div>beautiful</div><div>world!</div>'
);
const divsCount = await page.$$eval(
'allArray/div',
(divs) => divs.length
);
expect(divsCount).toBe(3);
});
it('$$eval should accept extra arguments', async () => {
const { page } = getTestState();
await page.setContent(
'<div>hello</div><div>beautiful</div><div>world!</div>'
);
const divsCountPlus5 = await page.$$eval(
'allArray/div',
(divs, two: number, three: number) => divs.length + two + three,
2,
3
);
expect(divsCountPlus5).toBe(8);
});
it('$$eval should accept ElementHandles as arguments', async () => {
const { page } = getTestState();
await page.setContent(
'<section>2</section><section>2</section><section>1</section><div>3</div>'
);
const divHandle = await page.$('div');
const sum = await page.$$eval(
'allArray/section',
(sections, div: HTMLElement) =>
sections.reduce(
(acc, section) => acc + Number(section.textContent),
0
) + Number(div.textContent),
divHandle
);
expect(sum).toBe(8);
});
it('$$eval should handle many elements', async () => {
const { page } = getTestState();
await page.evaluate(
`
for (var i = 0; i <= 1000; i++) {
const section = document.createElement('section');
section.textContent = i;
document.body.appendChild(section);
}
`
);
const sum = await page.$$eval('allArray/section', (sections) =>
sections.reduce((acc, section) => acc + Number(section.textContent), 0)
);
expect(sum).toBe(500500);
});
});
});