chore: expose new locator apis on window.playwright (#18595)
This commit is contained in:
parent
05471df8bb
commit
8432d1592f
|
|
@ -18,8 +18,10 @@
|
||||||
import { assert } from '../utils';
|
import { assert } from '../utils';
|
||||||
import type * as channels from '@protocol/channels';
|
import type * as channels from '@protocol/channels';
|
||||||
import { ChannelOwner } from './channelOwner';
|
import { ChannelOwner } from './channelOwner';
|
||||||
import { FrameLocator, getByAltTextSelector, getByLabelSelector, getByPlaceholderSelector, getByRoleSelector, getByTestIdSelector, getByTextSelector, getByTitleSelector, Locator } from './locator';
|
import { FrameLocator, Locator, testIdAttributeName } from './locator';
|
||||||
import type { ByRoleOptions, LocatorOptions } from './locator';
|
import type { LocatorOptions } from './locator';
|
||||||
|
import { getByAltTextSelector, getByLabelSelector, getByPlaceholderSelector, getByRoleSelector, getByTestIdSelector, getByTextSelector, getByTitleSelector } from '../utils/isomorphic/locatorUtils';
|
||||||
|
import type { ByRoleOptions } from '../utils/isomorphic/locatorUtils';
|
||||||
import { ElementHandle, convertSelectOptionValues, convertInputFiles } from './elementHandle';
|
import { ElementHandle, convertSelectOptionValues, convertInputFiles } from './elementHandle';
|
||||||
import { assertMaxArguments, JSHandle, serializeArgument, parseResult } from './jsHandle';
|
import { assertMaxArguments, JSHandle, serializeArgument, parseResult } from './jsHandle';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
|
@ -306,7 +308,7 @@ export class Frame extends ChannelOwner<channels.FrameChannel> implements api.Fr
|
||||||
}
|
}
|
||||||
|
|
||||||
getByTestId(testId: string): Locator {
|
getByTestId(testId: string): Locator {
|
||||||
return this.locator(getByTestIdSelector(testId));
|
return this.locator(getByTestIdSelector(testIdAttributeName, testId));
|
||||||
}
|
}
|
||||||
|
|
||||||
getByAltText(text: string | RegExp, options?: { exact?: boolean }): Locator {
|
getByAltText(text: string | RegExp, options?: { exact?: boolean }): Locator {
|
||||||
|
|
|
||||||
|
|
@ -19,29 +19,20 @@ import type * as api from '../../types/types';
|
||||||
import type * as channels from '@protocol/channels';
|
import type * as channels from '@protocol/channels';
|
||||||
import type { ParsedStackTrace } from '../utils/stackTrace';
|
import type { ParsedStackTrace } from '../utils/stackTrace';
|
||||||
import * as util from 'util';
|
import * as util from 'util';
|
||||||
import { isString, monotonicTime } from '../utils';
|
import { monotonicTime } from '../utils';
|
||||||
import { ElementHandle } from './elementHandle';
|
import { ElementHandle } from './elementHandle';
|
||||||
import type { Frame } from './frame';
|
import type { Frame } from './frame';
|
||||||
import type { FilePayload, FrameExpectOptions, Rect, SelectOption, SelectOptionOptions, TimeoutOptions } from './types';
|
import type { FilePayload, FrameExpectOptions, Rect, SelectOption, SelectOptionOptions, TimeoutOptions } from './types';
|
||||||
import { parseResult, serializeArgument } from './jsHandle';
|
import { parseResult, serializeArgument } from './jsHandle';
|
||||||
import { escapeForAttributeSelector, escapeForTextSelector } from '../utils/isomorphic/stringUtils';
|
import { escapeForTextSelector } from '../utils/isomorphic/stringUtils';
|
||||||
|
import type { ByRoleOptions } from '../utils/isomorphic/locatorUtils';
|
||||||
|
import { getByAltTextSelector, getByLabelSelector, getByPlaceholderSelector, getByRoleSelector, getByTestIdSelector, getByTextSelector, getByTitleSelector } from '../utils/isomorphic/locatorUtils';
|
||||||
|
|
||||||
export type LocatorOptions = {
|
export type LocatorOptions = {
|
||||||
hasText?: string | RegExp;
|
hasText?: string | RegExp;
|
||||||
has?: Locator;
|
has?: Locator;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ByRoleOptions = LocatorOptions & {
|
|
||||||
checked?: boolean;
|
|
||||||
disabled?: boolean;
|
|
||||||
expanded?: boolean;
|
|
||||||
includeHidden?: boolean;
|
|
||||||
level?: number;
|
|
||||||
name?: string | RegExp;
|
|
||||||
pressed?: boolean;
|
|
||||||
selected?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class Locator implements api.Locator {
|
export class Locator implements api.Locator {
|
||||||
_frame: Frame;
|
_frame: Frame;
|
||||||
_selector: string;
|
_selector: string;
|
||||||
|
|
@ -143,7 +134,7 @@ export class Locator implements api.Locator {
|
||||||
}
|
}
|
||||||
|
|
||||||
getByTestId(testId: string): Locator {
|
getByTestId(testId: string): Locator {
|
||||||
return this.locator(getByTestIdSelector(testId));
|
return this.locator(getByTestIdSelector(testIdAttributeName, testId));
|
||||||
}
|
}
|
||||||
|
|
||||||
getByAltText(text: string | RegExp, options?: { exact?: boolean }): Locator {
|
getByAltText(text: string | RegExp, options?: { exact?: boolean }): Locator {
|
||||||
|
|
@ -349,7 +340,7 @@ export class FrameLocator implements api.FrameLocator {
|
||||||
}
|
}
|
||||||
|
|
||||||
getByTestId(testId: string): Locator {
|
getByTestId(testId: string): Locator {
|
||||||
return this.locator(getByTestIdSelector(testId));
|
return this.locator(getByTestIdSelector(testIdAttributeName, testId));
|
||||||
}
|
}
|
||||||
|
|
||||||
getByAltText(text: string | RegExp, options?: { exact?: boolean }): Locator {
|
getByAltText(text: string | RegExp, options?: { exact?: boolean }): Locator {
|
||||||
|
|
@ -393,60 +384,8 @@ export class FrameLocator implements api.FrameLocator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let testIdAttributeName: string = 'data-testid';
|
export let testIdAttributeName: string = 'data-testid';
|
||||||
|
|
||||||
export function setTestIdAttribute(attributeName: string) {
|
export function setTestIdAttribute(attributeName: string) {
|
||||||
testIdAttributeName = attributeName;
|
testIdAttributeName = attributeName;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getByAttributeTextSelector(attrName: string, text: string | RegExp, options?: { exact?: boolean }): string {
|
|
||||||
if (!isString(text))
|
|
||||||
return `internal:attr=[${attrName}=${text}]`;
|
|
||||||
return `internal:attr=[${attrName}=${escapeForAttributeSelector(text, options?.exact || false)}]`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getByTestIdSelector(testId: string): string {
|
|
||||||
return getByAttributeTextSelector(testIdAttributeName, testId, { exact: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function getByLabelSelector(text: string | RegExp, options?: { exact?: boolean }): string {
|
|
||||||
return 'internal:label=' + escapeForTextSelector(text, !!options?.exact);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getByAltTextSelector(text: string | RegExp, options?: { exact?: boolean }): string {
|
|
||||||
return getByAttributeTextSelector('alt', text, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getByTitleSelector(text: string | RegExp, options?: { exact?: boolean }): string {
|
|
||||||
return getByAttributeTextSelector('title', text, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getByPlaceholderSelector(text: string | RegExp, options?: { exact?: boolean }): string {
|
|
||||||
return getByAttributeTextSelector('placeholder', text, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getByTextSelector(text: string | RegExp, options?: { exact?: boolean }): string {
|
|
||||||
return 'internal:text=' + escapeForTextSelector(text, !!options?.exact);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getByRoleSelector(role: string, options: ByRoleOptions = {}): string {
|
|
||||||
const props: string[][] = [];
|
|
||||||
if (options.checked !== undefined)
|
|
||||||
props.push(['checked', String(options.checked)]);
|
|
||||||
if (options.disabled !== undefined)
|
|
||||||
props.push(['disabled', String(options.disabled)]);
|
|
||||||
if (options.selected !== undefined)
|
|
||||||
props.push(['selected', String(options.selected)]);
|
|
||||||
if (options.expanded !== undefined)
|
|
||||||
props.push(['expanded', String(options.expanded)]);
|
|
||||||
if (options.includeHidden !== undefined)
|
|
||||||
props.push(['include-hidden', String(options.includeHidden)]);
|
|
||||||
if (options.level !== undefined)
|
|
||||||
props.push(['level', String(options.level)]);
|
|
||||||
if (options.name !== undefined)
|
|
||||||
props.push(['name', isString(options.name) ? escapeForAttributeSelector(options.name, false) : String(options.name)]);
|
|
||||||
if (options.pressed !== undefined)
|
|
||||||
props.push(['pressed', String(options.pressed)]);
|
|
||||||
return `internal:role=${role}${props.map(([n, v]) => `[${n}=${v}]`).join('')}`;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,8 @@ import { Frame, verifyLoadState } from './frame';
|
||||||
import { HarRouter } from './harRouter';
|
import { HarRouter } from './harRouter';
|
||||||
import { Keyboard, Mouse, Touchscreen } from './input';
|
import { Keyboard, Mouse, Touchscreen } from './input';
|
||||||
import { assertMaxArguments, JSHandle, parseResult, serializeArgument } from './jsHandle';
|
import { assertMaxArguments, JSHandle, parseResult, serializeArgument } from './jsHandle';
|
||||||
import type { ByRoleOptions, FrameLocator, Locator, LocatorOptions } from './locator';
|
import type { FrameLocator, Locator, LocatorOptions } from './locator';
|
||||||
|
import type { ByRoleOptions } from '../utils/isomorphic/locatorUtils';
|
||||||
import type { RouteHandlerCallback } from './network';
|
import type { RouteHandlerCallback } from './network';
|
||||||
import { Response, Route, RouteHandler, validateHeaders, WebSocket } from './network';
|
import { Response, Route, RouteHandler, validateHeaders, WebSocket } from './network';
|
||||||
import type { Request } from './network';
|
import type { Request } from './network';
|
||||||
|
|
|
||||||
|
|
@ -14,40 +14,54 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { ByRoleOptions } from '../../utils/isomorphic/locatorUtils';
|
||||||
|
import { getByAltTextSelector, getByLabelSelector, getByPlaceholderSelector, getByRoleSelector, getByTestIdSelector, getByTextSelector, getByTitleSelector } from '../../utils/isomorphic/locatorUtils';
|
||||||
import { escapeForTextSelector } from '../../utils/isomorphic/stringUtils';
|
import { escapeForTextSelector } from '../../utils/isomorphic/stringUtils';
|
||||||
|
import { asLocator } from '../isomorphic/locatorGenerators';
|
||||||
|
import type { Language } from '../isomorphic/locatorGenerators';
|
||||||
import { type InjectedScript } from './injectedScript';
|
import { type InjectedScript } from './injectedScript';
|
||||||
import { generateSelector } from './selectorGenerator';
|
import { generateSelector } from './selectorGenerator';
|
||||||
|
|
||||||
function createLocator(injectedScript: InjectedScript, initial: string, options?: { hasText?: string | RegExp }) {
|
const selectorSymbol = Symbol('selector');
|
||||||
class Locator {
|
const injectedScriptSymbol = Symbol('injectedScript');
|
||||||
selector: string;
|
|
||||||
element: Element | undefined;
|
|
||||||
elements: Element[];
|
|
||||||
|
|
||||||
constructor(selector: string, options?: { hasText?: string | RegExp, has?: Locator }) {
|
class Locator {
|
||||||
this.selector = selector;
|
element: Element | undefined;
|
||||||
if (options?.hasText)
|
elements: Element[] | undefined;
|
||||||
this.selector += ` >> internal:has-text=${escapeForTextSelector(options.hasText, false)}`;
|
|
||||||
if (options?.has)
|
constructor(injectedScript: InjectedScript, selector: string, options?: { hasText?: string | RegExp, has?: Locator }) {
|
||||||
this.selector += ` >> internal:has=` + JSON.stringify(options.has.selector);
|
(this as any)[selectorSymbol] = selector;
|
||||||
const parsed = injectedScript.parseSelector(this.selector);
|
(this as any)[injectedScriptSymbol] = injectedScript;
|
||||||
|
if (options?.hasText)
|
||||||
|
selector += ` >> internal:has-text=${escapeForTextSelector(options.hasText, false)}`;
|
||||||
|
if (options?.has)
|
||||||
|
selector += ` >> internal:has=` + JSON.stringify((options.has as any)[selectorSymbol]);
|
||||||
|
if (selector) {
|
||||||
|
const parsed = injectedScript.parseSelector(selector);
|
||||||
this.element = injectedScript.querySelector(parsed, document, false);
|
this.element = injectedScript.querySelector(parsed, document, false);
|
||||||
this.elements = injectedScript.querySelectorAll(parsed, document);
|
this.elements = injectedScript.querySelectorAll(parsed, document);
|
||||||
}
|
}
|
||||||
|
const selectorBase = selector;
|
||||||
locator(selector: string, options?: { hasText: string | RegExp, has?: Locator }): Locator {
|
const self = this as any;
|
||||||
return new Locator(this.selector ? this.selector + ' >> ' + selector : selector, options);
|
self.locator = (selector: string, options?: { hasText?: string | RegExp, has?: Locator }): Locator => {
|
||||||
}
|
return new Locator(injectedScript, selectorBase ? selectorBase + ' >> ' + selector : selector, options);
|
||||||
|
};
|
||||||
|
self.getByTestId = (testId: string): Locator => self.locator(getByTestIdSelector('data-testid', testId));
|
||||||
|
self.getByAltText = (text: string | RegExp, options?: { exact?: boolean }): Locator => self.locator(getByAltTextSelector(text, options));
|
||||||
|
self.getByLabel = (text: string | RegExp, options?: { exact?: boolean }): Locator => self.locator(getByLabelSelector(text, options));
|
||||||
|
self.getByPlaceholder = (text: string | RegExp, options?: { exact?: boolean }): Locator => self.locator(getByPlaceholderSelector(text, options));
|
||||||
|
self.getByText = (text: string | RegExp, options?: { exact?: boolean }): Locator => self.locator(getByTextSelector(text, options));
|
||||||
|
self.getByTitle = (text: string | RegExp, options?: { exact?: boolean }): Locator => self.locator(getByTitleSelector(text, options));
|
||||||
|
self.getByRole = (role: string, options: ByRoleOptions = {}): Locator => self.locator(getByRoleSelector(role, options));
|
||||||
}
|
}
|
||||||
return new Locator(initial, options);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConsoleAPIInterface = {
|
type ConsoleAPIInterface = {
|
||||||
$: (selector: string) => void;
|
$: (selector: string) => void;
|
||||||
$$: (selector: string) => void;
|
$$: (selector: string) => void;
|
||||||
locator: (selector: string, options?: { hasText: string | RegExp, has?: any }) => any;
|
|
||||||
inspect: (selector: string) => void;
|
inspect: (selector: string) => void;
|
||||||
selector: (element: Element) => void;
|
selector: (element: Element) => void;
|
||||||
|
generateLocator: (element: Element, language?: Language) => void;
|
||||||
resume: () => void;
|
resume: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -69,10 +83,11 @@ class ConsoleAPI {
|
||||||
window.playwright = {
|
window.playwright = {
|
||||||
$: (selector: string, strict?: boolean) => this._querySelector(selector, !!strict),
|
$: (selector: string, strict?: boolean) => this._querySelector(selector, !!strict),
|
||||||
$$: (selector: string) => this._querySelectorAll(selector),
|
$$: (selector: string) => this._querySelectorAll(selector),
|
||||||
locator: (selector: string, options?: { hasText?: string | RegExp }) => createLocator(this._injectedScript, selector, options),
|
|
||||||
inspect: (selector: string) => this._inspect(selector),
|
inspect: (selector: string) => this._inspect(selector),
|
||||||
selector: (element: Element) => this._selector(element),
|
selector: (element: Element) => this._selector(element),
|
||||||
|
generateLocator: (element: Element, language?: Language) => this._generateLocator(element, language),
|
||||||
resume: () => this._resume(),
|
resume: () => this._resume(),
|
||||||
|
...new Locator(injectedScript, ''),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -102,6 +117,13 @@ class ConsoleAPI {
|
||||||
return generateSelector(this._injectedScript, element, true).selector;
|
return generateSelector(this._injectedScript, element, true).selector;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _generateLocator(element: Element, language?: Language) {
|
||||||
|
if (!(element instanceof Element))
|
||||||
|
throw new Error(`Usage: playwright.locator(element).`);
|
||||||
|
const selector = generateSelector(this._injectedScript, element, true).selector;
|
||||||
|
return asLocator(language || 'javascript', selector);
|
||||||
|
}
|
||||||
|
|
||||||
private _resume() {
|
private _resume() {
|
||||||
window.__pw_resume().catch(() => {});
|
window.__pw_resume().catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
/**
|
||||||
|
* 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 { escapeForAttributeSelector, escapeForTextSelector, isString } from './stringUtils';
|
||||||
|
|
||||||
|
export type ByRoleOptions = {
|
||||||
|
checked?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
expanded?: boolean;
|
||||||
|
includeHidden?: boolean;
|
||||||
|
level?: number;
|
||||||
|
name?: string | RegExp;
|
||||||
|
pressed?: boolean;
|
||||||
|
selected?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getByAttributeTextSelector(attrName: string, text: string | RegExp, options?: { exact?: boolean }): string {
|
||||||
|
if (!isString(text))
|
||||||
|
return `internal:attr=[${attrName}=${text}]`;
|
||||||
|
return `internal:attr=[${attrName}=${escapeForAttributeSelector(text, options?.exact || false)}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getByTestIdSelector(testIdAttributeName: string, testId: string): string {
|
||||||
|
return getByAttributeTextSelector(testIdAttributeName, testId, { exact: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getByLabelSelector(text: string | RegExp, options?: { exact?: boolean }): string {
|
||||||
|
return 'internal:label=' + escapeForTextSelector(text, !!options?.exact);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getByAltTextSelector(text: string | RegExp, options?: { exact?: boolean }): string {
|
||||||
|
return getByAttributeTextSelector('alt', text, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getByTitleSelector(text: string | RegExp, options?: { exact?: boolean }): string {
|
||||||
|
return getByAttributeTextSelector('title', text, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getByPlaceholderSelector(text: string | RegExp, options?: { exact?: boolean }): string {
|
||||||
|
return getByAttributeTextSelector('placeholder', text, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getByTextSelector(text: string | RegExp, options?: { exact?: boolean }): string {
|
||||||
|
return 'internal:text=' + escapeForTextSelector(text, !!options?.exact);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getByRoleSelector(role: string, options: ByRoleOptions = {}): string {
|
||||||
|
const props: string[][] = [];
|
||||||
|
if (options.checked !== undefined)
|
||||||
|
props.push(['checked', String(options.checked)]);
|
||||||
|
if (options.disabled !== undefined)
|
||||||
|
props.push(['disabled', String(options.disabled)]);
|
||||||
|
if (options.selected !== undefined)
|
||||||
|
props.push(['selected', String(options.selected)]);
|
||||||
|
if (options.expanded !== undefined)
|
||||||
|
props.push(['expanded', String(options.expanded)]);
|
||||||
|
if (options.includeHidden !== undefined)
|
||||||
|
props.push(['include-hidden', String(options.includeHidden)]);
|
||||||
|
if (options.level !== undefined)
|
||||||
|
props.push(['level', String(options.level)]);
|
||||||
|
if (options.name !== undefined)
|
||||||
|
props.push(['name', isString(options.name) ? escapeForAttributeSelector(options.name, false) : String(options.name)]);
|
||||||
|
if (options.pressed !== undefined)
|
||||||
|
props.push(['pressed', String(options.pressed)]);
|
||||||
|
return `internal:role=${role}${props.map(([n, v]) => `[${n}=${v}]`).join('')}`;
|
||||||
|
}
|
||||||
|
|
@ -27,6 +27,10 @@ export function escapeWithQuotes(text: string, char: string = '\'') {
|
||||||
throw new Error('Invalid escape char');
|
throw new Error('Invalid escape char');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isString(obj: any): obj is string {
|
||||||
|
return typeof obj === 'string' || obj instanceof String;
|
||||||
|
}
|
||||||
|
|
||||||
export function toTitleCase(name: string) {
|
export function toTitleCase(name: string) {
|
||||||
return name.charAt(0).toUpperCase() + name.substring(1);
|
return name.charAt(0).toUpperCase() + name.substring(1);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue