chore: implement common functions for BiDi (#10345)

This commit is contained in:
Nikolay Vitkov 2023-06-12 11:32:19 +02:00 committed by GitHub
parent e3e68a99d2
commit a31231ef54
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 260 additions and 201 deletions

View File

@ -29,7 +29,7 @@ import {
NodeFor,
} from '../common/types.js';
import {KeyInput} from '../common/USKeyboardLayout.js';
import {withSourcePuppeteerURLIfNone} from '../common/util.js';
import {isString, withSourcePuppeteerURLIfNone} from '../common/util.js';
import {assert} from '../util/assert.js';
import {AsyncIterableUtil} from '../util/AsyncIterableUtil.js';
@ -428,9 +428,11 @@ export class ElementHandle<
* If there are no such elements, the method will resolve to an empty array.
* @param expression - Expression to {@link https://developer.mozilla.org/en-US/docs/Web/API/Document/evaluate | evaluate}
*/
async $x(expression: string): Promise<Array<ElementHandle<Node>>>;
async $x(): Promise<Array<ElementHandle<Node>>> {
throw new Error('Not implemented');
async $x(expression: string): Promise<Array<ElementHandle<Node>>> {
if (expression.startsWith('//')) {
expression = `.${expression}`;
}
return this.$$(`xpath/${expression}`);
}
/**
@ -472,12 +474,15 @@ export class ElementHandle<
*/
async waitForSelector<Selector extends string>(
selector: Selector,
options?: WaitForSelectorOptions
): Promise<ElementHandle<NodeFor<Selector>> | null>;
async waitForSelector<Selector extends string>(): Promise<ElementHandle<
NodeFor<Selector>
> | null> {
throw new Error('Not implemented');
options: WaitForSelectorOptions = {}
): Promise<ElementHandle<NodeFor<Selector>> | null> {
const {updatedSelector, QueryHandler} =
getQueryHandlerAndSelector(selector);
return (await QueryHandler.waitFor(
this,
updatedSelector,
options
)) as ElementHandle<NodeFor<Selector>> | null;
}
/**
@ -591,11 +596,14 @@ export class ElementHandle<
*/
async toElement<
K extends keyof HTMLElementTagNameMap | keyof SVGElementTagNameMap
>(tagName: K): Promise<HandleFor<ElementFor<K>>>;
async toElement<
K extends keyof HTMLElementTagNameMap | keyof SVGElementTagNameMap
>(): Promise<HandleFor<ElementFor<K>>> {
throw new Error('Not implemented');
>(tagName: K): Promise<HandleFor<ElementFor<K>>> {
const isMatchingTagName = await this.evaluate((node, tagName) => {
return node.nodeName === tagName.toUpperCase();
}, tagName);
if (!isMatchingTagName) {
throw new Error(`Element is not a(n) \`${tagName}\` element`);
}
return this as unknown as HandleFor<ElementFor<K>>;
}
/**
@ -708,9 +716,48 @@ export class ElementHandle<
* `multiple` attribute, all values are considered, otherwise only the first
* one is taken into account.
*/
async select(...values: string[]): Promise<string[]>;
async select(): Promise<string[]> {
throw new Error('Not implemented');
async select(...values: string[]): Promise<string[]> {
for (const value of values) {
assert(
isString(value),
'Values must be strings. Found value "' +
value +
'" of type "' +
typeof value +
'"'
);
}
return this.evaluate((element, vals): string[] => {
const values = new Set(vals);
if (!(element instanceof HTMLSelectElement)) {
throw new Error('Element is not a <select> element.');
}
const selectedValues = new Set<string>();
if (!element.multiple) {
for (const option of element.options) {
option.selected = false;
}
for (const option of element.options) {
if (values.has(option.value)) {
option.selected = true;
selectedValues.add(option.value);
break;
}
}
} else {
for (const option of element.options) {
option.selected = values.has(option.value);
if (option.selected) {
selectedValues.add(option.value);
}
}
}
element.dispatchEvent(new Event('input', {bubbles: true}));
element.dispatchEvent(new Event('change', {bubbles: true}));
return [...selectedValues.values()];
}, values);
}
/**
@ -757,7 +804,12 @@ export class ElementHandle<
* Calls {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus | focus} on the element.
*/
async focus(): Promise<void> {
throw new Error('Not implemented');
await this.evaluate(element => {
if (!(element instanceof HTMLElement)) {
throw new Error('Cannot focus non-HTMLElement');
}
return element.focus();
});
}
/**
@ -908,7 +960,14 @@ export class ElementHandle<
* or by calling element.scrollIntoView.
*/
async scrollIntoView(this: ElementHandle<Element>): Promise<void> {
throw new Error('Not implemented');
await this.assertConnectedElement();
await this.evaluate(async (element): Promise<void> => {
element.scrollIntoView({
block: 'center',
inline: 'center',
behavior: 'instant',
});
});
}
/**

View File

@ -1125,9 +1125,8 @@ export class Page extends EventEmitter {
*
* @param expression - Expression to evaluate
*/
async $x(expression: string): Promise<Array<ElementHandle<Node>>>;
async $x(): Promise<Array<ElementHandle<Node>>> {
throw new Error('Not implemented');
async $x(expression: string): Promise<Array<ElementHandle<Node>>> {
return this.mainFrame().$x(expression);
}
/**
@ -2642,12 +2641,9 @@ export class Page extends EventEmitter {
*/
async waitForSelector<Selector extends string>(
selector: Selector,
options?: WaitForSelectorOptions
): Promise<ElementHandle<NodeFor<Selector>> | null>;
async waitForSelector<Selector extends string>(): Promise<ElementHandle<
NodeFor<Selector>
> | null> {
throw new Error('Not implemented');
options: WaitForSelectorOptions = {}
): Promise<ElementHandle<NodeFor<Selector>> | null> {
return await this.mainFrame().waitForSelector(selector, options);
}
/**

View File

@ -107,7 +107,10 @@ export interface SnapshotOptions {
root?: ElementHandle<Node>;
}
interface DataProvider {
/**
* @internal
*/
export interface DataProvider {
getFullAXTree(): Promise<Protocol.Accessibility.GetFullAXTreeResponse>;
describeNode(id: string): Promise<Protocol.DOM.DescribeNodeResponse>;
}

View File

@ -32,15 +32,14 @@ import {CDPSession} from './Connection.js';
import {ExecutionContext} from './ExecutionContext.js';
import {Frame} from './Frame.js';
import {FrameManager} from './FrameManager.js';
import {getQueryHandlerAndSelector} from './GetQueryHandler.js';
import {WaitForSelectorOptions} from './IsolatedWorld.js';
import {PUPPETEER_WORLD} from './IsolatedWorlds.js';
import {CDPJSHandle} from './JSHandle.js';
import {LazyArg} from './LazyArg.js';
import {CDPPage} from './Page.js';
import {ElementFor, HandleFor, NodeFor} from './types.js';
import {NodeFor} from './types.js';
import {KeyInput} from './USKeyboardLayout.js';
import {debugError, isString} from './util.js';
import {debugError} from './util.js';
const applyOffsetsToQuad = (
quad: Point[],
@ -120,26 +119,13 @@ export class CDPElementHandle<
>;
}
override async $x(
expression: string
): Promise<Array<CDPElementHandle<Node>>> {
if (expression.startsWith('//')) {
expression = `.${expression}`;
}
return this.$$(`xpath/${expression}`);
}
override async waitForSelector<Selector extends string>(
selector: Selector,
options: WaitForSelectorOptions = {}
options?: WaitForSelectorOptions
): Promise<CDPElementHandle<NodeFor<Selector>> | null> {
const {updatedSelector, QueryHandler} =
getQueryHandlerAndSelector(selector);
return (await QueryHandler.waitFor(
this,
updatedSelector,
options
)) as CDPElementHandle<NodeFor<Selector>> | null;
return (await super.waitForSelector(selector, options)) as CDPElementHandle<
NodeFor<Selector>
> | null;
}
override async waitForXPath(
@ -182,18 +168,6 @@ export class CDPElementHandle<
return this.#checkVisibility(false);
}
override async toElement<
K extends keyof HTMLElementTagNameMap | keyof SVGElementTagNameMap
>(tagName: K): Promise<HandleFor<ElementFor<K>>> {
const isMatchingTagName = await this.evaluate((node, tagName) => {
return node.nodeName === tagName.toUpperCase();
}, tagName);
if (!isMatchingTagName) {
throw new Error(`Element is not a(n) \`${tagName}\` element`);
}
return this as unknown as HandleFor<ElementFor<K>>;
}
override async contentFrame(): Promise<Frame | null> {
const nodeInfo = await this.client.send('DOM.describeNode', {
objectId: this.id,
@ -208,7 +182,6 @@ export class CDPElementHandle<
this: CDPElementHandle<Element>
): Promise<void> {
await this.assertConnectedElement();
try {
await this.client.send('DOM.scrollIntoViewIfNeeded', {
objectId: this.id,
@ -216,13 +189,7 @@ export class CDPElementHandle<
} catch (error) {
debugError(error);
// Fallback to Element.scrollIntoView if DOM.scrollIntoViewIfNeeded is not supported
await this.evaluate(async (element): Promise<void> => {
element.scrollIntoView({
block: 'center',
inline: 'center',
behavior: 'instant',
});
});
await super.scrollIntoView();
}
}
@ -452,50 +419,6 @@ export class CDPElementHandle<
await this.#page.mouse.dragAndDrop(startPoint, targetPoint, options);
}
override async select(...values: string[]): Promise<string[]> {
for (const value of values) {
assert(
isString(value),
'Values must be strings. Found value "' +
value +
'" of type "' +
typeof value +
'"'
);
}
return this.evaluate((element, vals): string[] => {
const values = new Set(vals);
if (!(element instanceof HTMLSelectElement)) {
throw new Error('Element is not a <select> element.');
}
const selectedValues = new Set<string>();
if (!element.multiple) {
for (const option of element.options) {
option.selected = false;
}
for (const option of element.options) {
if (values.has(option.value)) {
option.selected = true;
selectedValues.add(option.value);
break;
}
}
} else {
for (const option of element.options) {
option.selected = values.has(option.value);
if (option.selected) {
selectedValues.add(option.value);
}
}
}
element.dispatchEvent(new Event('input', {bubbles: true}));
element.dispatchEvent(new Event('change', {bubbles: true}));
return [...selectedValues.values()];
}, values);
}
override async uploadFile(
this: CDPElementHandle<HTMLInputElement>,
...filePaths: string[]
@ -577,15 +500,6 @@ export class CDPElementHandle<
await this.#page.touchscreen.touchEnd();
}
override async focus(): Promise<void> {
await this.evaluate(element => {
if (!(element instanceof HTMLElement)) {
throw new Error('Cannot focus non-HTMLElement');
}
return element.focus();
});
}
override async type(text: string, options?: {delay: number}): Promise<void> {
await this.focus();
await this.#page.keyboard.type(text, options);

View File

@ -531,12 +531,6 @@ export class Frame extends BaseFrame {
return this.worlds[PUPPETEER_WORLD].type(selector, text, options);
}
override waitForTimeout(milliseconds: number): Promise<void> {
return new Promise(resolve => {
setTimeout(resolve, milliseconds);
});
}
override async title(): Promise<string> {
return this.worlds[PUPPETEER_WORLD].title();
}

View File

@ -76,7 +76,7 @@ import {TargetManagerEmittedEvents} from './TargetManager.js';
import {TaskQueue} from './TaskQueue.js';
import {TimeoutSettings} from './TimeoutSettings.js';
import {Tracing} from './Tracing.js';
import {BindingPayload, EvaluateFunc, HandleFor, NodeFor} from './types.js';
import {BindingPayload, EvaluateFunc, HandleFor} from './types.js';
import {
createClientError,
createJSHandle,
@ -554,10 +554,6 @@ export class CDPPage extends Page {
return createJSHandle(context, response.objects) as HandleFor<Prototype[]>;
}
override async $x(expression: string): Promise<Array<ElementHandle<Node>>> {
return this.mainFrame().$x(expression);
}
override async cookies(
...urls: string[]
): Promise<Protocol.Network.Cookie[]> {
@ -1557,13 +1553,6 @@ export class CDPPage extends Page {
return this.mainFrame().waitForTimeout(milliseconds);
}
override async waitForSelector<Selector extends string>(
selector: Selector,
options: WaitForSelectorOptions = {}
): Promise<ElementHandle<NodeFor<Selector>> | null> {
return await this.mainFrame().waitForSelector(selector, options);
}
override waitForXPath(
xpath: string,
options: WaitForSelectorOptions = {}

View File

@ -22,6 +22,7 @@ import {
Browser as BrowserBase,
BrowserCloseCallback,
BrowserContextOptions,
BrowserEmittedEvents,
} from '../../api/Browser.js';
import {BrowserContext as BrowserContextBase} from '../../api/BrowserContext.js';
import {Viewport} from '../PuppeteerViewport.js';
@ -59,6 +60,10 @@ export class Browser extends BrowserBase {
this.#closeCallback = opts.closeCallback;
this.#connection = opts.connection;
this.#defaultViewport = opts.defaultViewport;
this.#process?.on('close', () => {
return this.emit(BrowserEmittedEvents.Disconnected);
});
}
get connection(): Connection {

View File

@ -94,4 +94,8 @@ export class BrowserContext extends BrowserContextBase {
override browser(): Browser {
return this.#browser;
}
override async pages(): Promise<PageBase[]> {
return [...this.#pages.values()];
}
}

View File

@ -177,6 +177,10 @@ export class Frame extends BaseFrame {
return this.sandboxes[MAIN_SANDBOX].$$eval(selector, pageFunction, ...args);
}
override $x(expression: string): Promise<Array<ElementHandle<Node>>> {
return this.sandboxes[MAIN_SANDBOX].$x(expression);
}
dispose(): void {
this.#context.dispose();
}

View File

@ -112,4 +112,9 @@ export class Sandbox {
const document = await this.document();
return document.$$eval(selector, pageFunction, ...args);
}
async $x(expression: string): Promise<Array<ElementHandle<Node>>> {
const document = await this.document();
return document.$x(expression);
}
}

View File

@ -11,6 +11,18 @@
"parameters": ["webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[elementhandle.spec] ElementHandle specs Custom queries *",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.isIntersectingViewport *",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[evaluation.spec] *",
"platforms": ["darwin", "linux", "win32"],
@ -203,6 +215,12 @@
"parameters": ["webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[queryselector.spec] querySelector *",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[accessibility.spec] *",
"platforms": ["darwin", "linux", "win32"],
@ -257,6 +275,30 @@
"parameters": ["webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[elementhandle.spec] ElementHandle specs Custom queries should wait correctly with waitFor",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[elementhandle.spec] ElementHandle specs Custom queries should wait correctly with waitForSelector",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[elementhandle.spec] ElementHandle specs Custom queries should wait correctly with waitForSelector on an element",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[elementhandle.spec] ElementHandle specs Element.toElement should work",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.isVisible and ElementHandle.isHidden should work",
"platforms": ["darwin", "linux", "win32"],
@ -372,15 +414,9 @@
"expectations": ["SKIP"]
},
{
"testIdPattern": "[jshandle.spec] JSHandle JSHandle.jsonValue should not work with dates",
"testIdPattern": "[jshandle.spec] JSHandle JSHandle.jsonValue should work with dates",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[jshandle.spec] JSHandle JSHandle.toString should work for complicated objects",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"parameters": ["cdp"],
"expectations": ["FAIL"]
},
{
@ -413,6 +449,12 @@
"parameters": ["chrome"],
"expectations": ["SKIP"]
},
{
"testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch tmp profile should be cleaned up",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[navigation.spec] navigation \"after each\" hook for \"should work with both domcontentloaded and load\"",
"platforms": ["darwin", "linux", "win32"],
@ -695,6 +737,18 @@
"parameters": ["cdp", "firefox"],
"expectations": ["FAIL", "PASS"]
},
{
"testIdPattern": "[chromiumonly.spec] Chromium-Specific Launcher tests Puppeteer.launch |browserURL| option should be able to connect using browserUrl, with and without trailing slash",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[chromiumonly.spec] Chromium-Specific Launcher tests Puppeteer.launch |browserURL| option should throw when trying to connect to non-existing browser",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[click.spec] Page.click should click on checkbox label and toggle",
"platforms": ["darwin", "linux", "win32"],
@ -1019,12 +1073,6 @@
"parameters": ["cdp", "firefox"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[jshandle.spec] JSHandle JSHandle.jsonValue should not work with dates",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["cdp", "firefox"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[jshandle.spec] JSHandle JSHandle.toString should work with different subtypes",
"platforms": ["darwin", "linux", "win32"],
@ -1193,6 +1241,12 @@
"parameters": ["cdp", "firefox"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch tmp profile should be cleaned up",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch userDataDir argument with non-existent dir",
"platforms": ["darwin", "linux", "win32"],

View File

@ -467,12 +467,23 @@ describe('ElementHandle specs', function () {
it('should work', async () => {
const {page, server} = getTestState();
async function getVisibilityForButton(selector: string) {
const button = (await page.$(selector))!;
return await button.isIntersectingViewport();
}
await page.goto(server.PREFIX + '/offscreenbuttons.html');
const buttonsPromises = [];
// Firefox seems slow when using `isIntersectingViewport`
// so we do all the tasks asynchronously
for (let i = 0; i < 11; ++i) {
buttonsPromises.push(getVisibilityForButton('#btn' + i));
}
const buttonVisibility = await Promise.all(buttonsPromises);
for (let i = 0; i < 11; ++i) {
const button = (await page.$('#btn' + i))!;
// All but last button are visible.
const visible = i < 10;
expect(await button.isIntersectingViewport()).toBe(visible);
expect(buttonVisibility[i]).toBe(visible);
}
});
it('should work with threshold', async () => {
@ -505,51 +516,69 @@ describe('ElementHandle specs', function () {
const {page, server} = getTestState();
await page.goto(server.PREFIX + '/inline-svg.html');
const visibleCircle = await page.$('circle');
const visibleSvg = await page.$('svg');
expect(
await visibleCircle!.isIntersectingViewport({
threshold: 1,
})
).toBe(true);
expect(
await visibleCircle!.isIntersectingViewport({
threshold: 0,
})
).toBe(true);
expect(
await visibleSvg!.isIntersectingViewport({
threshold: 1,
})
).toBe(true);
expect(
await visibleSvg!.isIntersectingViewport({
threshold: 0,
})
).toBe(true);
const [visibleCircle, visibleSvg] = await Promise.all([
page.$('circle'),
page.$('svg'),
]);
const invisibleCircle = await page.$('div circle');
const invisibleSvg = await page.$('div svg');
expect(
await invisibleCircle!.isIntersectingViewport({
// Firefox seems slow when using `isIntersectingViewport`
// so we do all the tasks asynchronously
const [
circleThresholdOne,
circleThresholdZero,
svgThresholdOne,
svgThresholdZero,
] = await Promise.all([
visibleCircle!.isIntersectingViewport({
threshold: 1,
})
).toBe(false);
expect(
await invisibleCircle!.isIntersectingViewport({
}),
visibleCircle!.isIntersectingViewport({
threshold: 0,
})
).toBe(false);
expect(
await invisibleSvg!.isIntersectingViewport({
}),
visibleSvg!.isIntersectingViewport({
threshold: 1,
})
).toBe(false);
expect(
await invisibleSvg!.isIntersectingViewport({
}),
visibleSvg!.isIntersectingViewport({
threshold: 0,
})
).toBe(false);
}),
]);
expect(circleThresholdOne).toBe(true);
expect(circleThresholdZero).toBe(true);
expect(svgThresholdOne).toBe(true);
expect(svgThresholdZero).toBe(true);
const [invisibleCircle, invisibleSvg] = await Promise.all([
page.$('div circle'),
await page.$('div svg'),
]);
// Firefox seems slow when using `isIntersectingViewport`
// so we do all the tasks asynchronously
const [
invisibleCircleThresholdOne,
invisibleCircleThresholdZero,
invisibleSvgThresholdOne,
invisibleSvgThresholdZero,
] = await Promise.all([
invisibleCircle!.isIntersectingViewport({
threshold: 1,
}),
invisibleCircle!.isIntersectingViewport({
threshold: 0,
}),
invisibleSvg!.isIntersectingViewport({
threshold: 1,
}),
invisibleSvg!.isIntersectingViewport({
threshold: 0,
}),
]);
expect(invisibleCircleThresholdOne).toBe(false);
expect(invisibleCircleThresholdZero).toBe(false);
expect(invisibleSvgThresholdOne).toBe(false);
expect(invisibleSvgThresholdZero).toBe(false);
});
});

View File

@ -158,14 +158,14 @@ describe('JSHandle', function () {
expect(await bHandle.jsonValue()).toEqual(undefined);
});
it('should not work with dates', async () => {
it('should work with dates', async () => {
const {page} = getTestState();
const dateHandle = await page.evaluateHandle(() => {
return new Date('2017-09-26T00:00:00.000Z');
});
const json = await dateHandle.jsonValue();
expect(json).toEqual({});
const date = await dateHandle.jsonValue();
expect(date.toISOString()).toEqual('2017-09-26T00:00:00.000Z');
});
it('should throw for circular objects', async () => {
const {page} = getTestState();
@ -277,7 +277,10 @@ describe('JSHandle', function () {
const aHandle = await page.evaluateHandle(() => {
return window;
});
expect(aHandle.toString()).toBe('JSHandle@object');
expect(aHandle.toString()).atLeastOneToContain([
'JSHandle@object',
'JSHandle@window',
]);
});
it('should work with different subtypes', async () => {
const {page} = getTestState();