chore: move locators to strict selectors (#7866)

This commit is contained in:
Pavel Feldman 2021-07-27 15:27:36 -07:00 committed by GitHub
parent 982f61d575
commit b9aad5eb86
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 134 additions and 84 deletions

View File

@ -70,11 +70,6 @@ await element.HoverAsync();
await element.ClickAsync();
```
## async method: Locator.all
- returns: <[Array]<[ElementHandle]>>
Resolves given locator to all matching DOM elements.
## async method: Locator.boundingBox
- returns: <[null]|[Object]>
- `x` <[float]> the x coordinate of the element in pixels.
@ -167,6 +162,13 @@ When all steps combined have not finished during the specified [`option: timeout
### option: Locator.click.timeout = %%-input-timeout-%%
### option: Locator.click.trial = %%-input-trial-%%
## async method: Locator.count
- returns: <[int]>
Returns the number of elements matching given selector.
### option: Locator.elementHandle.elementHandle = %%-input-timeout-%%
## async method: Locator.dblclick
* langs:
- alias-csharp: DblClickAsync
@ -284,6 +286,19 @@ Optional event-specific initialization properties.
### option: Locator.dispatchEvent.timeout = %%-input-timeout-%%
## async method: Locator.elementHandle
- returns: <[ElementHandle]>
Resolves given locator to the first matching DOM element. If no elements matching the query are visible, waits for them up to a given timeout. If multiple elements match the selector, throws.
### option: Locator.elementHandle.elementHandle = %%-input-timeout-%%
## async method: Locator.elementHandles
- returns: <[Array]<[ElementHandle]>>
Resolves given locator to all matching DOM elements.
## async method: Locator.evaluate
- returns: <[Serializable]>
@ -414,13 +429,10 @@ Value to set for the `<input>`, `<textarea>` or `[contenteditable]` element.
### option: Locator.fill.noWaitAfter = %%-input-no-wait-after-%%
### option: Locator.fill.timeout = %%-input-timeout-%%
## async method: Locator.first
- returns: <[ElementHandle]>
## method: Locator.first
- returns: <[Locator]>
Resolves given locator to the first VISIBLE matching DOM element. If no elements matching
the query are visible, waits for them up to a given timeout.
### option: Locator.first.timeout = %%-input-timeout-%%
Returns locator to the first matching element.
## async method: Locator.focus

View File

@ -27,7 +27,7 @@ import { Page } from './page';
import { EventEmitter } from 'events';
import { Waiter } from './waiter';
import { Events } from './events';
import { LifecycleEvent, URLMatch, SelectOption, SelectOptionOptions, FilePayload, WaitForFunctionOptions, kLifecycleEvents } from './types';
import { LifecycleEvent, URLMatch, SelectOption, SelectOptionOptions, FilePayload, WaitForFunctionOptions, kLifecycleEvents, StrictOptions } from './types';
import { urlMatches } from './clientHelper';
import * as api from '../../types/types';
import * as structs from '../../types/structs';
@ -401,7 +401,7 @@ export class Frame extends ChannelOwner<channels.FrameChannel, channels.FrameIni
});
}
async selectOption(selector: string, values: string | api.ElementHandle | SelectOption | string[] | api.ElementHandle[] | SelectOption[] | null, options: SelectOptionOptions = {}): Promise<string[]> {
async selectOption(selector: string, values: string | api.ElementHandle | SelectOption | string[] | api.ElementHandle[] | SelectOption[] | null, options: SelectOptionOptions & StrictOptions = {}): Promise<string[]> {
return this._wrapApiCall(async (channel: channels.FrameChannel) => {
return (await channel.selectOption({ selector, ...convertSelectOptionValues(values), ...options })).values;
});

View File

@ -26,166 +26,178 @@ import { FilePayload, Rect, SelectOption, SelectOptionOptions, TimeoutOptions }
export class Locator implements api.Locator {
private _frame: Frame;
private _selector: string;
private _visibleSelector: string;
constructor(frame: Frame, selector: string) {
this._frame = frame;
this._selector = selector;
this._visibleSelector = selector + ' >> _visible=true';
}
private async _withFirst<R, O extends TimeoutOptions>(task: (handle: ElementHandle<SVGElement | HTMLElement>, options?: O) => Promise<R>, options?: O): Promise<R> {
private async _withElement<R, O extends TimeoutOptions>(task: (handle: ElementHandle<SVGElement | HTMLElement>, options?: O) => Promise<R>, options?: O): Promise<R> {
if (!options)
options = {} as any;
const timeout = this._frame.page()._timeoutSettings.timeout(options!);
const deadline = timeout ? monotonicTime() + timeout : 0;
const first = await this.first(options);
const handle = await this.elementHandle(options);
if (!handle)
throw new Error(`Could not resolve ${this._selector} to DOM Element`);
try {
return await task(first, { ...options!, timeout: deadline ? deadline - monotonicTime() : 0 });
return await task(handle, { ...options!, timeout: deadline ? deadline - monotonicTime() : 0 });
} finally {
first.dispose();
handle.dispose();
}
}
async all(): Promise<api.ElementHandle<SVGElement | HTMLElement>[]> {
return this._frame.$$(this._selector);
}
async boundingBox(options?: TimeoutOptions): Promise<Rect | null> {
return this._withFirst(h => h.boundingBox(), options);
return this._withElement(h => h.boundingBox(), { strict: true, ...options });
}
async check(options: channels.ElementHandleCheckOptions = {}) {
return this._frame.check(this._selector, options);
return this._frame.check(this._visibleSelector, { strict: true, ...options });
}
async click(options: channels.ElementHandleClickOptions = {}): Promise<void> {
return this._frame.click(this._selector, options);
return this._frame.click(this._visibleSelector, { strict: true, ...options });
}
async dblclick(options: channels.ElementHandleDblclickOptions = {}): Promise<void> {
return this._frame.dblclick(this._selector, options);
return this._frame.dblclick(this._visibleSelector, { strict: true, ...options });
}
async dispatchEvent(type: string, eventInit: Object = {}, options?: TimeoutOptions) {
return this._frame.dispatchEvent(this._selector, type, eventInit, options);
return this._frame.dispatchEvent(this._visibleSelector, type, eventInit, { strict: true, ...options });
}
async evaluate<R, Arg>(pageFunction: structs.PageFunctionOn<SVGElement | HTMLElement, Arg, R>, arg?: Arg, options?: TimeoutOptions): Promise<R> {
return this._withFirst(h => h.evaluate(pageFunction as any, arg), options);
return this._withElement(h => h.evaluate(pageFunction as any, arg), { strict: true, ...options });
}
async evaluateAll<R, Arg>(pageFunction: structs.PageFunctionOn<(SVGElement | HTMLElement)[], Arg, R>, arg?: Arg): Promise<R> {
return this._frame.$$eval(this._selector, pageFunction as any, arg);
return this._frame.$$eval(this._visibleSelector, pageFunction as any, arg);
}
async evaluateHandle<R, Arg>(pageFunction: structs.PageFunction<Arg, R>, arg?: Arg, options?: TimeoutOptions): Promise<structs.SmartHandle<R>> {
return this._withFirst(h => h.evaluateHandle(pageFunction as any, arg), options);
return this._withElement(h => h.evaluateHandle(pageFunction as any, arg), { strict: true, ...options });
}
async fill(value: string, options: channels.ElementHandleFillOptions = {}): Promise<void> {
return this._frame.fill(this._selector, value, options);
return this._frame.fill(this._visibleSelector, value, { strict: true, ...options });
}
locator(selector: string): Locator {
return new Locator(this._frame, this._selector + ' >> ' + selector);
}
async first(options?: TimeoutOptions): Promise<ElementHandle<SVGElement | HTMLElement>> {
const result = await this._frame.waitForSelector(this._selector, options);
async elementHandle(options?: TimeoutOptions): Promise<ElementHandle<SVGElement | HTMLElement>> {
const result = await this._frame.waitForSelector(this._visibleSelector, { strict: true, state: 'attached', ...options });
return result!;
}
async elementHandles(): Promise<api.ElementHandle<SVGElement | HTMLElement>[]> {
return this._frame.$$(this._visibleSelector);
}
first(): Locator {
return new Locator(this._frame, this._selector + ' >> _first=true');
}
async focus(options?: TimeoutOptions): Promise<void> {
return this._frame.focus(this._selector, options);
return this._frame.focus(this._visibleSelector, { strict: true, ...options });
}
async count(): Promise<number> {
return this.evaluateAll(ee => ee.length);
}
async getAttribute(name: string, options?: TimeoutOptions): Promise<string | null> {
return this._frame.getAttribute(this._selector, name, options);
return this._frame.getAttribute(this._visibleSelector, name, { strict: true, ...options });
}
async hover(options: channels.ElementHandleHoverOptions = {}): Promise<void> {
return this._frame.hover(this._selector, options);
return this._frame.hover(this._visibleSelector, { strict: true, ...options });
}
async innerHTML(options?: TimeoutOptions): Promise<string> {
return this._frame.innerHTML(this._selector, options);
return this._frame.innerHTML(this._visibleSelector, { strict: true, ...options });
}
async innerText(options?: TimeoutOptions): Promise<string> {
return this._frame.innerText(this._selector, options);
return this._frame.innerText(this._visibleSelector, { strict: true, ...options });
}
async inputValue(options?: TimeoutOptions): Promise<string> {
return this._frame.inputValue(this._selector, options);
return this._frame.inputValue(this._visibleSelector, { strict: true, ...options });
}
async isChecked(options?: TimeoutOptions): Promise<boolean> {
return this._frame.isChecked(this._selector, options);
return this._frame.isChecked(this._visibleSelector, { strict: true, ...options });
}
async isDisabled(options?: TimeoutOptions): Promise<boolean> {
return this._frame.isDisabled(this._selector, options);
return this._frame.isDisabled(this._visibleSelector, { strict: true, ...options });
}
async isEditable(options?: TimeoutOptions): Promise<boolean> {
return this._frame.isEditable(this._selector, options);
return this._frame.isEditable(this._visibleSelector, { strict: true, ...options });
}
async isEnabled(options?: TimeoutOptions): Promise<boolean> {
return this._frame.isEnabled(this._selector, options);
return this._frame.isEnabled(this._visibleSelector, { strict: true, ...options });
}
async isHidden(options?: TimeoutOptions): Promise<boolean> {
return this._frame.isHidden(this._selector);
return this._frame.isHidden(this._visibleSelector, { strict: true, ...options });
}
async isVisible(options?: TimeoutOptions): Promise<boolean> {
return this._frame.isVisible(this._selector);
return this._frame.isVisible(this._visibleSelector, { strict: true, ...options });
}
async press(key: string, options: channels.ElementHandlePressOptions = {}): Promise<void> {
return this._frame.press(this._selector, key, options);
return this._frame.press(this._visibleSelector, key, { strict: true, ...options });
}
async screenshot(options: channels.ElementHandleScreenshotOptions & { path?: string } = {}): Promise<Buffer> {
return this._withFirst((h, o) => h.screenshot(o), options);
return this._withElement((h, o) => h.screenshot(o), { strict: true, ...options });
}
async scrollIntoViewIfNeeded(options: channels.ElementHandleScrollIntoViewIfNeededOptions = {}) {
return this._withFirst((h, o) => h.scrollIntoViewIfNeeded(o), options);
return this._withElement((h, o) => h.scrollIntoViewIfNeeded(o), { strict: true, ...options });
}
async selectOption(values: string | api.ElementHandle | SelectOption | string[] | api.ElementHandle[] | SelectOption[] | null, options: SelectOptionOptions = {}): Promise<string[]> {
return this._frame.selectOption(this._selector, values, options);
return this._frame.selectOption(this._visibleSelector, values, { strict: true, ...options });
}
async selectText(options: channels.ElementHandleSelectTextOptions = {}): Promise<void> {
return this._withFirst((h, o) => h.selectText(o), options);
return this._withElement((h, o) => h.selectText(o), { strict: true, ...options });
}
async setInputFiles(files: string | FilePayload | string[] | FilePayload[], options: channels.ElementHandleSetInputFilesOptions = {}) {
return this._frame.setInputFiles(this._selector, files, options);
return this._frame.setInputFiles(this._visibleSelector, files, { strict: true, ...options });
}
async tap(options: channels.ElementHandleTapOptions = {}): Promise<void> {
return this._frame.tap(this._selector, options);
return this._frame.tap(this._visibleSelector, { strict: true, ...options });
}
async textContent(options?: TimeoutOptions): Promise<string | null> {
return this._frame.textContent(this._selector, options);
return this._frame.textContent(this._visibleSelector, { strict: true, ...options });
}
async type(text: string, options: channels.ElementHandleTypeOptions = {}): Promise<void> {
return this._frame.type(this._selector, text, options);
return this._frame.type(this._visibleSelector, text, { strict: true, ...options });
}
async uncheck(options: channels.ElementHandleUncheckOptions = {}) {
return this._frame.uncheck(this._selector, options);
return this._frame.uncheck(this._visibleSelector, { strict: true, ...options });
}
waitFor(options: channels.FrameWaitForSelectorOptions & { state: 'attached' | 'visible' }): Promise<ElementHandle<SVGElement | HTMLElement>>;
waitFor(options?: channels.FrameWaitForSelectorOptions): Promise<ElementHandle<SVGElement | HTMLElement> | null>;
async waitFor(options?: channels.FrameWaitForSelectorOptions): Promise<ElementHandle<SVGElement | HTMLElement> | null> {
return this._frame.waitForSelector(this._selector, options);
return this._frame.waitForSelector(this._visibleSelector, { strict: true, ...options });
}
[(util.inspect as any).custom]() {

View File

@ -25,7 +25,7 @@ export interface Logger {
import { Size } from '../common/types';
export { Size, Point, Rect, Quad, URLMatch, TimeoutOptions } from '../common/types';
export type StrictOptions = { strict?: boolean };
export type Headers = { [key: string]: string };
export type Env = { [key: string]: string | number | boolean | undefined };

View File

@ -43,6 +43,7 @@ export class Selectors {
'data-testid', 'data-testid:light',
'data-test-id', 'data-test-id:light',
'data-test', 'data-test:light',
'_visible', '_first'
]);
this._engines = new Map();
}

View File

@ -39,21 +39,6 @@ it('should work for TextNodes', async ({ page, server }) => {
expect(await page.evaluate(() => window['result'])).toBe('Clicked');
});
it('should throw for recursively hidden nodes with force', async ({ page, server }) => {
await page.goto(server.PREFIX + '/input/button.html');
const button = page.locator('button');
await page.evaluate(button => button.parentElement.style.display = 'none', await button.first());
const error = await button.click({ force: true }).catch(err => err);
expect(error.message).toContain('Element is not visible');
});
it('should throw for <br> elements with force', async ({ page, server }) => {
await page.setContent('hello<br>goodbye');
const br = page.locator('br');
const error = await br.click({ force: true }).catch(err => err);
expect(error.message).toContain('Element is outside of the viewport');
});
it('should double click the button', async ({ page, server }) => {
await page.goto(server.PREFIX + '/input/button.html');
await page.evaluate(() => {

View File

@ -23,14 +23,14 @@ it('should query existing element', async ({page, server}) => {
const html = page.locator('html');
const second = html.locator('.second');
const inner = second.locator('.inner');
const content = await page.evaluate(e => e.textContent, await inner.first());
const content = await page.evaluate(e => e.textContent, await inner.elementHandle());
expect(content).toBe('A');
});
it('should query existing elements', async ({page, server}) => {
await page.setContent('<html><body><div>A</div><br/><div>B</div></body></html>');
const html = page.locator('html');
const elements = await html.locator('div').all();
const elements = await html.locator('div').elementHandles();
expect(elements.length).toBe(2);
const promises = elements.map(element => page.evaluate(e => e.textContent, element));
expect(await Promise.all(promises)).toEqual(['A', 'B']);
@ -39,7 +39,7 @@ it('should query existing elements', async ({page, server}) => {
it('should return empty array for non-existing elements', async ({page, server}) => {
await page.setContent('<html><body><span>A</span><br/><span>B</span></body></html>');
const html = page.locator('html');
const elements = await html.locator('div').all();
const elements = await html.locator('div').elementHandles();
expect(elements.length).toBe(0);
});
@ -50,13 +50,13 @@ it('xpath should query existing element', async ({page, server}) => {
const html = page.locator('html');
const second = html.locator(`xpath=./body/div[contains(@class, 'second')]`);
const inner = second.locator(`xpath=./div[contains(@class, 'inner')]`);
const content = await page.evaluate(e => e.textContent, await inner.first());
const content = await page.evaluate(e => e.textContent, await inner.elementHandle());
expect(content).toBe('A');
});
it('xpath should return null for non-existing element', async ({page, server}) => {
await page.setContent('<html><body><div class="second"><div class="inner">B</div></div></body></html>');
const html = page.locator('html');
const second = await html.locator(`xpath=/div[contains(@class, 'third')]`).all();
const second = await html.locator(`xpath=/div[contains(@class, 'third')]`).elementHandles();
expect(second).toEqual([]);
});

View File

@ -88,9 +88,9 @@ it('should dispatch click event via ElementHandles', async ({page, server}) => {
it('should upload the file', async ({page, server, asset}) => {
await page.goto(server.PREFIX + '/input/fileupload.html');
const filePath = path.relative(process.cwd(), asset('file-to-upload.txt'));
const input = page.locator('input');
const input = page.locator('input[type=file]');
await input.setInputFiles(filePath);
expect(await page.evaluate(e => (e as HTMLInputElement).files[0].name, await input.first())).toBe('file-to-upload.txt');
expect(await page.evaluate(e => (e as HTMLInputElement).files[0].name, await input.elementHandle())).toBe('file-to-upload.txt');
});
it.describe('tap group', () => {

View File

@ -0,0 +1,30 @@
/**
* Copyright 2018 Google Inc. All rights reserved.
* Modifications copyright (c) Microsoft Corporation.
*
* 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 { test as it, expect } from './pageTest';
it('should respect first()', async ({page}) => {
await page.setContent(`
<section>
<div><p>A</p></div>
<div><p>A</p><p>A</p></div>
<div><p>A</p><p>A</p><p>A</p></div>
</section>`);
expect(await page.locator('div >> p').count()).toBe(6);
expect(await page.locator('div').locator('p').count()).toBe(6);
expect(await page.locator('div').first().locator('p').count()).toBe(1);
});

18
types/types.d.ts vendored
View File

@ -6963,17 +6963,17 @@ export interface ElementHandle<T=Node> extends JSHandle<T> {
*/
export interface Locator {
/**
* Resolves given locator to the first VISIBLE matching DOM element. If no elements matching the query are visible, waits
* for them up to a given timeout.
* Resolves given locator to the first matching DOM element. If no elements matching the query are visible, waits for them
* up to a given timeout. If multiple elements match the selector, throws.
* @param options
*/
first(options?: {
elementHandle(options?: {
timeout?: number;
}): Promise<null|ElementHandle<SVGElement | HTMLElement>>;
/**
* Resolves given locator to all matching DOM elements.
*/
all(): Promise<null|ElementHandle<SVGElement | HTMLElement>[]>;
elementHandles(): Promise<null|ElementHandle<SVGElement | HTMLElement>[]>;
/**
* Returns the return value of `pageFunction`.
*
@ -7192,6 +7192,11 @@ export interface Locator {
trial?: boolean;
}): Promise<void>;
/**
* Returns the number of elements matching given selector.
*/
count(): Promise<number>;
/**
* This method double clicks the element by performing the following steps:
* 1. Wait for [actionability](https://playwright.dev/docs/actionability) checks on the element, unless `force` option is set.
@ -7376,6 +7381,11 @@ export interface Locator {
timeout?: number;
}): Promise<void>;
/**
* Returns locator to the first matching element.
*/
first(): Locator;
/**
* Calls [focus](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus) on the element.
* @param options

View File

@ -141,10 +141,10 @@ export interface ElementHandle<T=Node> extends JSHandle<T> {
}
export interface Locator {
first(options?: {
elementHandle(options?: {
timeout?: number;
}): Promise<null|ElementHandle<SVGElement | HTMLElement>>;
all(): Promise<null|ElementHandle<SVGElement | HTMLElement>[]>;
elementHandles(): Promise<null|ElementHandle<SVGElement | HTMLElement>[]>;
evaluate<R, Arg>(pageFunction: PageFunctionOn<SVGElement | HTMLElement, Arg, R>, arg: Arg): Promise<R>;
evaluate<R>(pageFunction: PageFunctionOn<SVGElement | HTMLElement, void, R>): Promise<R>;
evaluateAll<R, Arg>(pageFunction: PageFunctionOn<(SVGElement | HTMLElement)[], Arg, R>, arg: Arg): Promise<R>;