mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore: internal selectors (#17827)
- Rename internal selectors `has`, `control` and `attr` to `internal:has`, `internal:control` and `internal:attr`. - Fix `getByLabel()` to respect strictness, by introducing `internal:label` selector. - Move tests essential for ports to `selectors-by.spec`.
This commit is contained in:
parent
f64dbe9ece
commit
2bcd9ce9ae
@ -57,12 +57,12 @@ export class Locator implements api.Locator {
|
|||||||
|
|
||||||
private static getByAttributeTextSelector(attrName: string, text: string | RegExp, options?: { exact?: boolean }): string {
|
private static getByAttributeTextSelector(attrName: string, text: string | RegExp, options?: { exact?: boolean }): string {
|
||||||
if (!isString(text))
|
if (!isString(text))
|
||||||
return `attr=[${attrName}=${text}]`;
|
return `internal:attr=[${attrName}=${text}]`;
|
||||||
return `attr=[${attrName}=${escapeForAttributeSelector(text)}${options?.exact ? 's' : 'i'}]`;
|
return `internal:attr=[${attrName}=${escapeForAttributeSelector(text)}${options?.exact ? 's' : 'i'}]`;
|
||||||
}
|
}
|
||||||
|
|
||||||
static getByLabelSelector(text: string | RegExp, options?: { exact?: boolean }): string {
|
static getByLabelSelector(text: string | RegExp, options?: { exact?: boolean }): string {
|
||||||
return Locator.getByTextSelector(text, options) + ' >> control=resolve-label';
|
return 'internal:label=' + escapeForTextSelector(text, !!options?.exact);
|
||||||
}
|
}
|
||||||
|
|
||||||
static getByAltTextSelector(text: string | RegExp, options?: { exact?: boolean }): string {
|
static getByAltTextSelector(text: string | RegExp, options?: { exact?: boolean }): string {
|
||||||
@ -108,14 +108,14 @@ export class Locator implements api.Locator {
|
|||||||
|
|
||||||
if (options?.hasText) {
|
if (options?.hasText) {
|
||||||
const textSelector = 'text=' + escapeForTextSelector(options.hasText, false);
|
const textSelector = 'text=' + escapeForTextSelector(options.hasText, false);
|
||||||
this._selector += ` >> has=${JSON.stringify(textSelector)}`;
|
this._selector += ` >> internal:has=${JSON.stringify(textSelector)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options?.has) {
|
if (options?.has) {
|
||||||
const locator = options.has;
|
const locator = options.has;
|
||||||
if (locator._frame !== frame)
|
if (locator._frame !== frame)
|
||||||
throw new Error(`Inner "has" locator must belong to the same frame.`);
|
throw new Error(`Inner "has" locator must belong to the same frame.`);
|
||||||
this._selector += ` >> has=` + JSON.stringify(locator._selector);
|
this._selector += ` >> internal:has=` + JSON.stringify(locator._selector);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -395,7 +395,7 @@ export class FrameLocator implements api.FrameLocator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
locator(selector: string, options?: { hasText?: string | RegExp }): Locator {
|
locator(selector: string, options?: { hasText?: string | RegExp }): Locator {
|
||||||
return new Locator(this._frame, this._frameSelector + ' >> control=enter-frame >> ' + selector, options);
|
return new Locator(this._frame, this._frameSelector + ' >> internal:control=enter-frame >> ' + selector, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
getByTestId(testId: string): Locator {
|
getByTestId(testId: string): Locator {
|
||||||
@ -427,7 +427,7 @@ export class FrameLocator implements api.FrameLocator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
frameLocator(selector: string): FrameLocator {
|
frameLocator(selector: string): FrameLocator {
|
||||||
return new FrameLocator(this._frame, this._frameSelector + ' >> control=enter-frame >> ' + selector);
|
return new FrameLocator(this._frame, this._frameSelector + ' >> internal:control=enter-frame >> ' + selector);
|
||||||
}
|
}
|
||||||
|
|
||||||
first(): FrameLocator {
|
first(): FrameLocator {
|
||||||
|
@ -1501,7 +1501,7 @@ export class Frame extends SdkObject {
|
|||||||
return this.retryWithProgress(progress, selector, options, async selectorInFrame => {
|
return this.retryWithProgress(progress, selector, options, async selectorInFrame => {
|
||||||
// Be careful, |this| can be different from |frame|.
|
// Be careful, |this| can be different from |frame|.
|
||||||
progress.log(`waiting for selector "${selector}"`);
|
progress.log(`waiting for selector "${selector}"`);
|
||||||
const { frame, info } = selectorInFrame || { frame: this, info: { parsed: { parts: [{ name: 'control', body: 'return-empty', source: 'control=return-empty' }] }, world: 'utility', strict: !!options.strict } };
|
const { frame, info } = selectorInFrame || { frame: this, info: { parsed: { parts: [{ name: 'internal:control', body: 'return-empty', source: 'internal:control=return-empty' }] }, world: 'utility', strict: !!options.strict } };
|
||||||
return await frame._scheduleRerunnableTaskInFrame(progress, info, callbackText, taskData, options);
|
return await frame._scheduleRerunnableTaskInFrame(progress, info, callbackText, taskData, options);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -28,10 +28,10 @@ function createLocator(injectedScript: InjectedScript, initial: string, options?
|
|||||||
this.selector = selector;
|
this.selector = selector;
|
||||||
if (options?.hasText) {
|
if (options?.hasText) {
|
||||||
const textSelector = 'text=' + escapeForTextSelector(options.hasText, false);
|
const textSelector = 'text=' + escapeForTextSelector(options.hasText, false);
|
||||||
this.selector += ` >> has=${JSON.stringify(textSelector)}`;
|
this.selector += ` >> internal:has=${JSON.stringify(textSelector)}`;
|
||||||
}
|
}
|
||||||
if (options?.has)
|
if (options?.has)
|
||||||
this.selector += ` >> has=` + JSON.stringify(options.has.selector);
|
this.selector += ` >> internal:has=` + JSON.stringify(options.has.selector);
|
||||||
const parsed = injectedScript.parseSelector(this.selector);
|
const parsed = injectedScript.parseSelector(this.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);
|
||||||
|
@ -22,7 +22,7 @@ import { RoleEngine } from './roleSelectorEngine';
|
|||||||
import { parseAttributeSelector } from '../isomorphic/selectorParser';
|
import { parseAttributeSelector } from '../isomorphic/selectorParser';
|
||||||
import type { NestedSelectorBody, ParsedSelector, ParsedSelectorPart } from '../isomorphic/selectorParser';
|
import type { NestedSelectorBody, ParsedSelector, ParsedSelectorPart } from '../isomorphic/selectorParser';
|
||||||
import { allEngineNames, parseSelector, stringifySelector } from '../isomorphic/selectorParser';
|
import { allEngineNames, parseSelector, stringifySelector } from '../isomorphic/selectorParser';
|
||||||
import { type TextMatcher, elementMatchesText, createRegexTextMatcher, createStrictTextMatcher, createLaxTextMatcher, elementText } from './selectorUtils';
|
import { type TextMatcher, elementMatchesText, createRegexTextMatcher, createStrictTextMatcher, createLaxTextMatcher, elementText, createStrictFullTextMatcher } from './selectorUtils';
|
||||||
import { SelectorEvaluatorImpl } from './selectorEvaluator';
|
import { SelectorEvaluatorImpl } from './selectorEvaluator';
|
||||||
import { enclosingShadowRootOrDocument, isElementVisible, parentElementOrShadowHost } from './domUtils';
|
import { enclosingShadowRootOrDocument, isElementVisible, parentElementOrShadowHost } from './domUtils';
|
||||||
import type { CSSComplexSelectorList } from '../isomorphic/cssParser';
|
import type { CSSComplexSelectorList } from '../isomorphic/cssParser';
|
||||||
@ -103,9 +103,10 @@ export class InjectedScript {
|
|||||||
this._engines.set('css', this._createCSSEngine());
|
this._engines.set('css', this._createCSSEngine());
|
||||||
this._engines.set('nth', { queryAll: () => [] });
|
this._engines.set('nth', { queryAll: () => [] });
|
||||||
this._engines.set('visible', this._createVisibleEngine());
|
this._engines.set('visible', this._createVisibleEngine());
|
||||||
this._engines.set('control', this._createControlEngine());
|
this._engines.set('internal:control', this._createControlEngine());
|
||||||
this._engines.set('has', this._createHasEngine());
|
this._engines.set('internal:has', this._createHasEngine());
|
||||||
this._engines.set('attr', this._createNamedAttributeEngine());
|
this._engines.set('internal:label', this._createLabelEngine());
|
||||||
|
this._engines.set('internal:attr', this._createNamedAttributeEngine());
|
||||||
|
|
||||||
for (const { name, engine } of customEngines)
|
for (const { name, engine } of customEngines)
|
||||||
this._engines.set(name, engine);
|
this._engines.set(name, engine);
|
||||||
@ -173,7 +174,7 @@ export class InjectedScript {
|
|||||||
const withHas: ParsedSelector = { parts: selector.parts.slice(0, selector.capture + 1) };
|
const withHas: ParsedSelector = { parts: selector.parts.slice(0, selector.capture + 1) };
|
||||||
if (selector.capture < selector.parts.length - 1) {
|
if (selector.capture < selector.parts.length - 1) {
|
||||||
const parsed: ParsedSelector = { parts: selector.parts.slice(selector.capture + 1) };
|
const parsed: ParsedSelector = { parts: selector.parts.slice(selector.capture + 1) };
|
||||||
const has: ParsedSelectorPart = { name: 'has', body: { parsed }, source: stringifySelector(parsed) };
|
const has: ParsedSelectorPart = { name: 'internal:has', body: { parsed }, source: stringifySelector(parsed) };
|
||||||
withHas.parts.push(has);
|
withHas.parts.push(has);
|
||||||
}
|
}
|
||||||
return this.querySelectorAll(withHas, root);
|
return this.querySelectorAll(withHas, root);
|
||||||
@ -243,7 +244,7 @@ export class InjectedScript {
|
|||||||
|
|
||||||
private _createTextEngine(shadow: boolean): SelectorEngine {
|
private _createTextEngine(shadow: boolean): SelectorEngine {
|
||||||
const queryList = (root: SelectorRoot, selector: string): Element[] => {
|
const queryList = (root: SelectorRoot, selector: string): Element[] => {
|
||||||
const { matcher, kind } = createTextMatcher(selector);
|
const { matcher, kind } = createTextMatcher(selector, false);
|
||||||
const result: Element[] = [];
|
const result: Element[] = [];
|
||||||
let lastDidNotMatchSelf: Element | null = null;
|
let lastDidNotMatchSelf: Element | null = null;
|
||||||
|
|
||||||
@ -273,6 +274,23 @@ export class InjectedScript {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _createLabelEngine(): SelectorEngine {
|
||||||
|
const evaluator = this._evaluator;
|
||||||
|
return {
|
||||||
|
queryAll: (root: SelectorRoot, selector: string): Element[] => {
|
||||||
|
const { matcher } = createTextMatcher(selector, true);
|
||||||
|
const result: Element[] = [];
|
||||||
|
const labels = this._evaluator._queryCSS({ scope: root as Document | Element, pierceShadow: true }, 'label') as HTMLLabelElement[];
|
||||||
|
for (const label of labels) {
|
||||||
|
const control = label.control;
|
||||||
|
if (control && matcher(elementText(evaluator._cacheText, label)))
|
||||||
|
result.push(control);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private _createNamedAttributeEngine(): SelectorEngine {
|
private _createNamedAttributeEngine(): SelectorEngine {
|
||||||
const queryList = (root: SelectorRoot, selector: string): Element[] => {
|
const queryList = (root: SelectorRoot, selector: string): Element[] => {
|
||||||
const parsed = parseAttributeSelector(selector, true);
|
const parsed = parseAttributeSelector(selector, true);
|
||||||
@ -305,10 +323,6 @@ export class InjectedScript {
|
|||||||
return [];
|
return [];
|
||||||
if (body === 'return-empty')
|
if (body === 'return-empty')
|
||||||
return [];
|
return [];
|
||||||
if (body === 'resolve-label') {
|
|
||||||
const control = (root as HTMLLabelElement).control;
|
|
||||||
return control ? [control] : [];
|
|
||||||
}
|
|
||||||
if (body === 'component') {
|
if (body === 'component') {
|
||||||
if (root.nodeType !== 1 /* Node.ELEMENT_NODE */)
|
if (root.nodeType !== 1 /* Node.ELEMENT_NODE */)
|
||||||
return [];
|
return [];
|
||||||
@ -316,7 +330,7 @@ export class InjectedScript {
|
|||||||
// However, when mounting fragments, return the root instead.
|
// However, when mounting fragments, return the root instead.
|
||||||
return [root.childElementCount === 1 ? root.firstElementChild! : root as Element];
|
return [root.childElementCount === 1 ? root.firstElementChild! : root as Element];
|
||||||
}
|
}
|
||||||
throw new Error(`Internal error, unknown control selector ${body}`);
|
throw new Error(`Internal error, unknown internal:control selector ${body}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -1280,7 +1294,7 @@ function unescape(s: string): string {
|
|||||||
return r.join('');
|
return r.join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
function createTextMatcher(selector: string): { matcher: TextMatcher, kind: 'regex' | 'strict' | 'lax' } {
|
function createTextMatcher(selector: string, strictMatchesFullText: boolean): { matcher: TextMatcher, kind: 'regex' | 'strict' | 'lax' } {
|
||||||
if (selector[0] === '/' && selector.lastIndexOf('/') > 0) {
|
if (selector[0] === '/' && selector.lastIndexOf('/') > 0) {
|
||||||
const lastSlash = selector.lastIndexOf('/');
|
const lastSlash = selector.lastIndexOf('/');
|
||||||
const matcher: TextMatcher = createRegexTextMatcher(selector.substring(1, lastSlash), selector.substring(lastSlash + 1));
|
const matcher: TextMatcher = createRegexTextMatcher(selector.substring(1, lastSlash), selector.substring(lastSlash + 1));
|
||||||
@ -1295,8 +1309,9 @@ function createTextMatcher(selector: string): { matcher: TextMatcher, kind: 'reg
|
|||||||
selector = unescape(selector.substring(1, selector.length - 1));
|
selector = unescape(selector.substring(1, selector.length - 1));
|
||||||
strict = true;
|
strict = true;
|
||||||
}
|
}
|
||||||
const matcher = strict ? createStrictTextMatcher(selector) : createLaxTextMatcher(selector);
|
if (strict)
|
||||||
return { matcher, kind: strict ? 'strict' : 'lax' };
|
return { matcher: strictMatchesFullText ? createStrictFullTextMatcher(selector) : createStrictTextMatcher(selector), kind: 'strict' };
|
||||||
|
return { matcher: createLaxTextMatcher(selector), kind: 'lax' };
|
||||||
}
|
}
|
||||||
|
|
||||||
class ExpectedTextMatcher {
|
class ExpectedTextMatcher {
|
||||||
|
@ -148,7 +148,7 @@ function buildCandidates(element: Element, accessibleNameCache: Map<Element, boo
|
|||||||
const candidates: SelectorToken[] = [];
|
const candidates: SelectorToken[] = [];
|
||||||
|
|
||||||
if (element.getAttribute('data-testid'))
|
if (element.getAttribute('data-testid'))
|
||||||
candidates.push({ engine: 'attr', selector: `[data-testid=${escapeForAttributeSelector(element.getAttribute('data-testid')!)}]`, score: 1 });
|
candidates.push({ engine: 'internal:attr', selector: `[data-testid=${escapeForAttributeSelector(element.getAttribute('data-testid')!)}]`, score: 1 });
|
||||||
|
|
||||||
for (const attr of ['data-test-id', 'data-test']) {
|
for (const attr of ['data-test-id', 'data-test']) {
|
||||||
if (element.getAttribute(attr))
|
if (element.getAttribute(attr))
|
||||||
@ -158,7 +158,7 @@ function buildCandidates(element: Element, accessibleNameCache: Map<Element, boo
|
|||||||
if (element.nodeName === 'INPUT') {
|
if (element.nodeName === 'INPUT') {
|
||||||
const input = element as HTMLInputElement;
|
const input = element as HTMLInputElement;
|
||||||
if (input.placeholder)
|
if (input.placeholder)
|
||||||
candidates.push({ engine: 'attr', selector: `[placeholder=${escapeForAttributeSelector(input.placeholder)}]`, score: 3 });
|
candidates.push({ engine: 'internal:attr', selector: `[placeholder=${escapeForAttributeSelector(input.placeholder)}]`, score: 3 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const ariaRole = getAriaRole(element);
|
const ariaRole = getAriaRole(element);
|
||||||
@ -171,7 +171,7 @@ function buildCandidates(element: Element, accessibleNameCache: Map<Element, boo
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (element.getAttribute('alt') && ['APPLET', 'AREA', 'IMG', 'INPUT'].includes(element.nodeName))
|
if (element.getAttribute('alt') && ['APPLET', 'AREA', 'IMG', 'INPUT'].includes(element.nodeName))
|
||||||
candidates.push({ engine: 'attr', selector: `[alt=${escapeForAttributeSelector(element.getAttribute('alt')!)}]`, score: 10 });
|
candidates.push({ engine: 'internal:attr', selector: `[alt=${escapeForAttributeSelector(element.getAttribute('alt')!)}]`, score: 10 });
|
||||||
|
|
||||||
if (element.getAttribute('name') && ['BUTTON', 'FORM', 'FIELDSET', 'FRAME', 'IFRAME', 'INPUT', 'KEYGEN', 'OBJECT', 'OUTPUT', 'SELECT', 'TEXTAREA', 'MAP', 'META', 'PARAM'].includes(element.nodeName))
|
if (element.getAttribute('name') && ['BUTTON', 'FORM', 'FIELDSET', 'FRAME', 'IFRAME', 'INPUT', 'KEYGEN', 'OBJECT', 'OUTPUT', 'SELECT', 'TEXTAREA', 'MAP', 'META', 'PARAM'].includes(element.nodeName))
|
||||||
candidates.push({ engine: 'css', selector: `${cssEscape(element.nodeName.toLowerCase())}[name=${quoteAttributeValue(element.getAttribute('name')!)}]`, score: 50 });
|
candidates.push({ engine: 'css', selector: `${cssEscape(element.nodeName.toLowerCase())}[name=${quoteAttributeValue(element.getAttribute('name')!)}]`, score: 50 });
|
||||||
|
@ -68,6 +68,13 @@ export function createStrictTextMatcher(text: string): TextMatcher {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createStrictFullTextMatcher(text: string): TextMatcher {
|
||||||
|
text = text.trim().replace(/\s+/g, ' ');
|
||||||
|
return (elementText: ElementText) => {
|
||||||
|
return elementText.full.trim().replace(/\s+/g, ' ') === text;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function createRegexTextMatcher(source: string, flags?: string): TextMatcher {
|
export function createRegexTextMatcher(source: string, flags?: string): TextMatcher {
|
||||||
const re = new RegExp(source, flags);
|
const re = new RegExp(source, flags);
|
||||||
return (elementText: ElementText) => {
|
return (elementText: ElementText) => {
|
||||||
|
@ -19,7 +19,7 @@ import { InvalidSelectorError, parseCSS } from './cssParser';
|
|||||||
export { InvalidSelectorError, isInvalidSelectorError } from './cssParser';
|
export { InvalidSelectorError, isInvalidSelectorError } from './cssParser';
|
||||||
|
|
||||||
export type NestedSelectorBody = { parsed: ParsedSelector, distance?: number };
|
export type NestedSelectorBody = { parsed: ParsedSelector, distance?: number };
|
||||||
const kNestedSelectorNames = new Set(['has', 'left-of', 'right-of', 'above', 'below', 'near']);
|
const kNestedSelectorNames = new Set(['internal:has', 'left-of', 'right-of', 'above', 'below', 'near']);
|
||||||
const kNestedSelectorNamesWithDistance = new Set(['left-of', 'right-of', 'above', 'below', 'near']);
|
const kNestedSelectorNamesWithDistance = new Set(['left-of', 'right-of', 'above', 'below', 'near']);
|
||||||
|
|
||||||
export type ParsedSelectorPart = {
|
export type ParsedSelectorPart = {
|
||||||
@ -70,7 +70,7 @@ export function parseSelector(selector: string): ParsedSelector {
|
|||||||
throw new Error(`Malformed selector: ${part.name}=` + part.body);
|
throw new Error(`Malformed selector: ${part.name}=` + part.body);
|
||||||
}
|
}
|
||||||
const result = { name: part.name, source: part.body, body: { parsed: parseSelector(innerSelector), distance } };
|
const result = { name: part.name, source: part.body, body: { parsed: parseSelector(innerSelector), distance } };
|
||||||
if (result.body.parsed.parts.some(part => part.name === 'control' && part.body === 'enter-frame'))
|
if (result.body.parsed.parts.some(part => part.name === 'internal:control' && part.body === 'enter-frame'))
|
||||||
throw new Error(`Frames are not allowed inside "${part.name}" selectors`);
|
throw new Error(`Frames are not allowed inside "${part.name}" selectors`);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@ -93,7 +93,7 @@ export function splitSelectorByFrame(selectorText: string): ParsedSelector[] {
|
|||||||
let chunkStartIndex = 0;
|
let chunkStartIndex = 0;
|
||||||
for (let i = 0; i < selector.parts.length; ++i) {
|
for (let i = 0; i < selector.parts.length; ++i) {
|
||||||
const part = selector.parts[i];
|
const part = selector.parts[i];
|
||||||
if (part.name === 'control' && part.body === 'enter-frame') {
|
if (part.name === 'internal:control' && part.body === 'enter-frame') {
|
||||||
if (!chunk.parts.length)
|
if (!chunk.parts.length)
|
||||||
throw new InvalidSelectorError('Selector cannot start with entering frame, select the iframe first');
|
throw new InvalidSelectorError('Selector cannot start with entering frame, select the iframe first');
|
||||||
result.push(chunk);
|
result.push(chunk);
|
||||||
|
@ -121,7 +121,7 @@ export function asLocator(generator: LanguageGenerator, selector: string, isFram
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (part.name === 'attr') {
|
if (part.name === 'internal:attr') {
|
||||||
const attrSelector = parseAttributeSelector(part.body as string, true);
|
const attrSelector = parseAttributeSelector(part.body as string, true);
|
||||||
const { name, value } = attrSelector.attributes[0];
|
const { name, value } = attrSelector.attributes[0];
|
||||||
if (name === 'data-testid') {
|
if (name === 'data-testid') {
|
||||||
|
@ -45,8 +45,8 @@ export class Selectors {
|
|||||||
'data-testid', 'data-testid:light',
|
'data-testid', 'data-testid:light',
|
||||||
'data-test-id', 'data-test-id:light',
|
'data-test-id', 'data-test-id:light',
|
||||||
'data-test', 'data-test:light',
|
'data-test', 'data-test:light',
|
||||||
'nth', 'visible', 'control', 'has',
|
'nth', 'visible', 'internal:control', 'internal:has',
|
||||||
'role', 'attr'
|
'role', 'internal:attr', 'internal:label'
|
||||||
]);
|
]);
|
||||||
this._builtinEnginesInMainWorld = new Set([
|
this._builtinEnginesInMainWorld = new Set([
|
||||||
'_react', '_vue',
|
'_react', '_vue',
|
||||||
|
@ -64,7 +64,7 @@ export function escapeForTextSelector(text: string | RegExp, exact: boolean, cas
|
|||||||
if (exact)
|
if (exact)
|
||||||
return '"' + text.replace(/["]/g, '\\"') + '"';
|
return '"' + text.replace(/["]/g, '\\"') + '"';
|
||||||
if (text.includes('"') || text.includes('>>') || text[0] === '/')
|
if (text.includes('"') || text.includes('>>') || text[0] === '/')
|
||||||
return `/.*${escapeForRegex(text).replace(/\s+/, '\\s+')}.*/` + (caseSensitive ? '' : 'i');
|
return `/${escapeForRegex(text).replace(/\s+/, '\\s+')}/` + (caseSensitive ? '' : 'i');
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,7 +124,7 @@ async function innerMount(page: Page, jsxOrType: JsxComponent | string, options:
|
|||||||
|
|
||||||
await window.playwrightMount(component, rootElement, hooksConfig);
|
await window.playwrightMount(component, rootElement, hooksConfig);
|
||||||
|
|
||||||
return '#root >> control=component';
|
return '#root >> internal:control=component';
|
||||||
}, { component, hooksConfig: options.hooksConfig });
|
}, { component, hooksConfig: options.hooksConfig });
|
||||||
return selector;
|
return selector;
|
||||||
}
|
}
|
||||||
|
@ -235,7 +235,7 @@ test.describe('cli codegen', () => {
|
|||||||
await recorder.setContentAndWait(`<div data-testid=testid onclick="console.log('click')">Submit</div>`);
|
await recorder.setContentAndWait(`<div data-testid=testid onclick="console.log('click')">Submit</div>`);
|
||||||
|
|
||||||
const selector = await recorder.hoverOverElement('div');
|
const selector = await recorder.hoverOverElement('div');
|
||||||
expect(selector).toBe('attr=[data-testid="testid"]');
|
expect(selector).toBe('internal:attr=[data-testid="testid"]');
|
||||||
|
|
||||||
const [message, sources] = await Promise.all([
|
const [message, sources] = await Promise.all([
|
||||||
page.waitForEvent('console', msg => msg.type() !== 'error'),
|
page.waitForEvent('console', msg => msg.type() !== 'error'),
|
||||||
@ -267,7 +267,7 @@ test.describe('cli codegen', () => {
|
|||||||
await recorder.setContentAndWait(`<input placeholder="Country"></input>`);
|
await recorder.setContentAndWait(`<input placeholder="Country"></input>`);
|
||||||
|
|
||||||
const selector = await recorder.hoverOverElement('input');
|
const selector = await recorder.hoverOverElement('input');
|
||||||
expect(selector).toBe('attr=[placeholder="Country"]');
|
expect(selector).toBe('internal:attr=[placeholder="Country"]');
|
||||||
|
|
||||||
const [sources] = await Promise.all([
|
const [sources] = await Promise.all([
|
||||||
recorder.waitForOutput('JavaScript', 'click'),
|
recorder.waitForOutput('JavaScript', 'click'),
|
||||||
@ -296,7 +296,7 @@ test.describe('cli codegen', () => {
|
|||||||
await recorder.setContentAndWait(`<input alt="Country"></input>`);
|
await recorder.setContentAndWait(`<input alt="Country"></input>`);
|
||||||
|
|
||||||
const selector = await recorder.hoverOverElement('input');
|
const selector = await recorder.hoverOverElement('input');
|
||||||
expect(selector).toBe('attr=[alt="Country"]');
|
expect(selector).toBe('internal:attr=[alt="Country"]');
|
||||||
|
|
||||||
const [sources] = await Promise.all([
|
const [sources] = await Promise.all([
|
||||||
recorder.waitForOutput('JavaScript', 'click'),
|
recorder.waitForOutput('JavaScript', 'click'),
|
||||||
|
@ -45,7 +45,7 @@ it.describe('selector generator', () => {
|
|||||||
|
|
||||||
it('should not escape spaces inside named attr selectors', async ({ page }) => {
|
it('should not escape spaces inside named attr selectors', async ({ page }) => {
|
||||||
await page.setContent(`<input placeholder="Foo b ar"/>`);
|
await page.setContent(`<input placeholder="Foo b ar"/>`);
|
||||||
expect(await generate(page, 'input')).toBe('attr=[placeholder=\"Foo b ar\"]');
|
expect(await generate(page, 'input')).toBe('internal:attr=[placeholder=\"Foo b ar\"]');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should generate text for <input type=button>', async ({ page }) => {
|
it('should generate text for <input type=button>', async ({ page }) => {
|
||||||
@ -60,17 +60,17 @@ it.describe('selector generator', () => {
|
|||||||
|
|
||||||
it('should escape text with >>', async ({ page }) => {
|
it('should escape text with >>', async ({ page }) => {
|
||||||
await page.setContent(`<div>text>>text</div>`);
|
await page.setContent(`<div>text>>text</div>`);
|
||||||
expect(await generate(page, 'div')).toBe('text=/.*text\\>\\>text.*/');
|
expect(await generate(page, 'div')).toBe('text=/text\\>\\>text/');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should escape text with quote', async ({ page }) => {
|
it('should escape text with quote', async ({ page }) => {
|
||||||
await page.setContent(`<div>text"text</div>`);
|
await page.setContent(`<div>text"text</div>`);
|
||||||
expect(await generate(page, 'div')).toBe('text=/.*text"text.*/');
|
expect(await generate(page, 'div')).toBe('text=/text"text/');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should escape text with slash', async ({ page }) => {
|
it('should escape text with slash', async ({ page }) => {
|
||||||
await page.setContent(`<div>/text</div>`);
|
await page.setContent(`<div>/text</div>`);
|
||||||
expect(await generate(page, 'div')).toBe('text=/.*\/text.*/');
|
expect(await generate(page, 'div')).toBe('text=/\/text/');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not use text for select', async ({ page }) => {
|
it('should not use text for select', async ({ page }) => {
|
||||||
@ -88,7 +88,7 @@ it.describe('selector generator', () => {
|
|||||||
|
|
||||||
it('should prefer data-testid', async ({ page }) => {
|
it('should prefer data-testid', async ({ page }) => {
|
||||||
await page.setContent(`<div>Text</div><div>Text</div><div data-testid=a>Text</div><div>Text</div>`);
|
await page.setContent(`<div>Text</div><div>Text</div><div data-testid=a>Text</div><div>Text</div>`);
|
||||||
expect(await generate(page, '[data-testid="a"]')).toBe('attr=[data-testid=\"a\"]');
|
expect(await generate(page, '[data-testid="a"]')).toBe('internal:attr=[data-testid=\"a\"]');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle first non-unique data-testid', async ({ page }) => {
|
it('should handle first non-unique data-testid', async ({ page }) => {
|
||||||
@ -99,7 +99,7 @@ it.describe('selector generator', () => {
|
|||||||
<div data-testid=a>
|
<div data-testid=a>
|
||||||
Text
|
Text
|
||||||
</div>`);
|
</div>`);
|
||||||
expect(await generate(page, 'div[mark="1"]')).toBe('attr=[data-testid=\"a\"] >> nth=0');
|
expect(await generate(page, 'div[mark="1"]')).toBe('internal:attr=[data-testid=\"a\"] >> nth=0');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle second non-unique data-testid', async ({ page }) => {
|
it('should handle second non-unique data-testid', async ({ page }) => {
|
||||||
@ -110,7 +110,7 @@ it.describe('selector generator', () => {
|
|||||||
<div data-testid=a mark=1>
|
<div data-testid=a mark=1>
|
||||||
Text
|
Text
|
||||||
</div>`);
|
</div>`);
|
||||||
expect(await generate(page, 'div[mark="1"]')).toBe(`attr=[data-testid=\"a\"] >> nth=1`);
|
expect(await generate(page, 'div[mark="1"]')).toBe(`internal:attr=[data-testid=\"a\"] >> nth=1`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use readable id', async ({ page }) => {
|
it('should use readable id', async ({ page }) => {
|
||||||
@ -232,7 +232,7 @@ it.describe('selector generator', () => {
|
|||||||
});
|
});
|
||||||
it('placeholder', async ({ page }) => {
|
it('placeholder', async ({ page }) => {
|
||||||
await page.setContent(`<input placeholder="foobar" type="text"/>`);
|
await page.setContent(`<input placeholder="foobar" type="text"/>`);
|
||||||
expect(await generate(page, 'input')).toBe('attr=[placeholder=\"foobar\"]');
|
expect(await generate(page, 'input')).toBe('internal:attr=[placeholder=\"foobar\"]');
|
||||||
});
|
});
|
||||||
it('type', async ({ page }) => {
|
it('type', async ({ page }) => {
|
||||||
await page.setContent(`<input type="text"/>`);
|
await page.setContent(`<input type="text"/>`);
|
||||||
|
@ -180,31 +180,3 @@ it('alias methods coverage', async ({ page }) => {
|
|||||||
await expect(page.locator('div').getByRole('button')).toHaveCount(1);
|
await expect(page.locator('div').getByRole('button')).toHaveCount(1);
|
||||||
await expect(page.mainFrame().locator('button')).toHaveCount(1);
|
await expect(page.mainFrame().locator('button')).toHaveCount(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getBy escaping', async ({ page }) => {
|
|
||||||
await page.setContent(`<label id=label for=control>Hello
|
|
||||||
wo"rld</label><input id=control />`);
|
|
||||||
await page.$eval('input', input => {
|
|
||||||
input.setAttribute('placeholder', 'hello\nwo"rld');
|
|
||||||
input.setAttribute('title', 'hello\nwo"rld');
|
|
||||||
input.setAttribute('alt', 'hello\nwo"rld');
|
|
||||||
});
|
|
||||||
await expect(page.getByText('hello\nwo"rld')).toHaveAttribute('id', 'label');
|
|
||||||
await expect(page.getByLabel('hello\nwo"rld')).toHaveAttribute('id', 'control');
|
|
||||||
await expect(page.getByPlaceholder('hello\nwo"rld')).toHaveAttribute('id', 'control');
|
|
||||||
await expect(page.getByAltText('hello\nwo"rld')).toHaveAttribute('id', 'control');
|
|
||||||
await expect(page.getByTitle('hello\nwo"rld')).toHaveAttribute('id', 'control');
|
|
||||||
|
|
||||||
await page.setContent(`<label id=label for=control>Hello
|
|
||||||
world</label><input id=control />`);
|
|
||||||
await page.$eval('input', input => {
|
|
||||||
input.setAttribute('placeholder', 'hello\nworld');
|
|
||||||
input.setAttribute('title', 'hello\nworld');
|
|
||||||
input.setAttribute('alt', 'hello\nworld');
|
|
||||||
});
|
|
||||||
await expect(page.getByText('hello\nworld')).toHaveAttribute('id', 'label');
|
|
||||||
await expect(page.getByLabel('hello\nworld')).toHaveAttribute('id', 'control');
|
|
||||||
await expect(page.getByPlaceholder('hello\nworld')).toHaveAttribute('id', 'control');
|
|
||||||
await expect(page.getByAltText('hello\nworld')).toHaveAttribute('id', 'control');
|
|
||||||
await expect(page.getByTitle('hello\nworld')).toHaveAttribute('id', 'control');
|
|
||||||
});
|
|
||||||
|
@ -49,7 +49,7 @@ async function routeIframe(page: Page) {
|
|||||||
it('should work for iframe @smoke', async ({ page, server }) => {
|
it('should work for iframe @smoke', async ({ page, server }) => {
|
||||||
await routeIframe(page);
|
await routeIframe(page);
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
const button = page.locator('iframe >> control=enter-frame >> button');
|
const button = page.locator('iframe >> internal:control=enter-frame >> button');
|
||||||
await button.waitFor();
|
await button.waitFor();
|
||||||
expect(await button.innerText()).toBe('Hello iframe');
|
expect(await button.innerText()).toBe('Hello iframe');
|
||||||
await expect(button).toHaveText('Hello iframe');
|
await expect(button).toHaveText('Hello iframe');
|
||||||
@ -60,7 +60,7 @@ it('should work for iframe (handle)', async ({ page, server }) => {
|
|||||||
await routeIframe(page);
|
await routeIframe(page);
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
const body = await page.$('body');
|
const body = await page.$('body');
|
||||||
const button = await body.waitForSelector('iframe >> control=enter-frame >> button');
|
const button = await body.waitForSelector('iframe >> internal:control=enter-frame >> button');
|
||||||
expect(await button.innerText()).toBe('Hello iframe');
|
expect(await button.innerText()).toBe('Hello iframe');
|
||||||
expect(await button.textContent()).toBe('Hello iframe');
|
expect(await button.textContent()).toBe('Hello iframe');
|
||||||
await button.click();
|
await button.click();
|
||||||
@ -69,7 +69,7 @@ it('should work for iframe (handle)', async ({ page, server }) => {
|
|||||||
it('should work for nested iframe', async ({ page, server }) => {
|
it('should work for nested iframe', async ({ page, server }) => {
|
||||||
await routeIframe(page);
|
await routeIframe(page);
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
const button = page.locator('iframe >> control=enter-frame >> iframe >> control=enter-frame >> button');
|
const button = page.locator('iframe >> internal:control=enter-frame >> iframe >> internal:control=enter-frame >> button');
|
||||||
await button.waitFor();
|
await button.waitFor();
|
||||||
expect(await button.innerText()).toBe('Hello nested iframe');
|
expect(await button.innerText()).toBe('Hello nested iframe');
|
||||||
await expect(button).toHaveText('Hello nested iframe');
|
await expect(button).toHaveText('Hello nested iframe');
|
||||||
@ -80,7 +80,7 @@ it('should work for nested iframe (handle)', async ({ page, server }) => {
|
|||||||
await routeIframe(page);
|
await routeIframe(page);
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
const body = await page.$('body');
|
const body = await page.$('body');
|
||||||
const button = await body.waitForSelector('iframe >> control=enter-frame >> iframe >> control=enter-frame >> button');
|
const button = await body.waitForSelector('iframe >> internal:control=enter-frame >> iframe >> internal:control=enter-frame >> button');
|
||||||
expect(await button.innerText()).toBe('Hello nested iframe');
|
expect(await button.innerText()).toBe('Hello nested iframe');
|
||||||
expect(await button.textContent()).toBe('Hello nested iframe');
|
expect(await button.textContent()).toBe('Hello nested iframe');
|
||||||
await button.click();
|
await button.click();
|
||||||
@ -89,35 +89,35 @@ it('should work for nested iframe (handle)', async ({ page, server }) => {
|
|||||||
it('should work for $ and $$', async ({ page, server }) => {
|
it('should work for $ and $$', async ({ page, server }) => {
|
||||||
await routeIframe(page);
|
await routeIframe(page);
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
const element = await page.$('iframe >> control=enter-frame >> button');
|
const element = await page.$('iframe >> internal:control=enter-frame >> button');
|
||||||
expect(await element.textContent()).toBe('Hello iframe');
|
expect(await element.textContent()).toBe('Hello iframe');
|
||||||
const elements = await page.$$('iframe >> control=enter-frame >> span');
|
const elements = await page.$$('iframe >> internal:control=enter-frame >> span');
|
||||||
expect(elements).toHaveLength(2);
|
expect(elements).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('$ should not wait for frame', async ({ page, server }) => {
|
it('$ should not wait for frame', async ({ page, server }) => {
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
expect(await page.$('iframe >> control=enter-frame >> canvas')).toBeFalsy();
|
expect(await page.$('iframe >> internal:control=enter-frame >> canvas')).toBeFalsy();
|
||||||
const body = await page.$('body');
|
const body = await page.$('body');
|
||||||
expect(await body.$('iframe >> control=enter-frame >> canvas')).toBeFalsy();
|
expect(await body.$('iframe >> internal:control=enter-frame >> canvas')).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('$$ should not wait for frame', async ({ page, server }) => {
|
it('$$ should not wait for frame', async ({ page, server }) => {
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
expect(await page.$$('iframe >> control=enter-frame >> canvas')).toHaveLength(0);
|
expect(await page.$$('iframe >> internal:control=enter-frame >> canvas')).toHaveLength(0);
|
||||||
const body = await page.$('body');
|
const body = await page.$('body');
|
||||||
expect(await body.$$('iframe >> control=enter-frame >> canvas')).toHaveLength(0);
|
expect(await body.$$('iframe >> internal:control=enter-frame >> canvas')).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('$eval should throw for missing frame', async ({ page, server }) => {
|
it('$eval should throw for missing frame', async ({ page, server }) => {
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
{
|
{
|
||||||
const error = await page.$eval('iframe >> control=enter-frame >> canvas', e => 1).catch(e => e);
|
const error = await page.$eval('iframe >> internal:control=enter-frame >> canvas', e => 1).catch(e => e);
|
||||||
expect(error.message).toContain('Error: failed to find element matching selector');
|
expect(error.message).toContain('Error: failed to find element matching selector');
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const body = await page.$('body');
|
const body = await page.$('body');
|
||||||
const error = await body.$eval('iframe >> control=enter-frame >> canvas', e => 1).catch(e => e);
|
const error = await body.$eval('iframe >> internal:control=enter-frame >> canvas', e => 1).catch(e => e);
|
||||||
expect(error.message).toContain('Error: failed to find element matching selector');
|
expect(error.message).toContain('Error: failed to find element matching selector');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -125,12 +125,12 @@ it('$eval should throw for missing frame', async ({ page, server }) => {
|
|||||||
it('$$eval should throw for missing frame', async ({ page, server }) => {
|
it('$$eval should throw for missing frame', async ({ page, server }) => {
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
{
|
{
|
||||||
const error = await page.$$eval('iframe >> control=enter-frame >> canvas', e => 1).catch(e => e);
|
const error = await page.$$eval('iframe >> internal:control=enter-frame >> canvas', e => 1).catch(e => e);
|
||||||
expect(error.message).toContain('Error: failed to find frame for selector');
|
expect(error.message).toContain('Error: failed to find frame for selector');
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const body = await page.$('body');
|
const body = await page.$('body');
|
||||||
const error = await body.$$eval('iframe >> control=enter-frame >> canvas', e => 1).catch(e => e);
|
const error = await body.$$eval('iframe >> internal:control=enter-frame >> canvas', e => 1).catch(e => e);
|
||||||
expect(error.message).toContain('Error: failed to find frame for selector');
|
expect(error.message).toContain('Error: failed to find frame for selector');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -139,16 +139,16 @@ it('should work for $ and $$ (handle)', async ({ page, server }) => {
|
|||||||
await routeIframe(page);
|
await routeIframe(page);
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
const body = await page.$('body');
|
const body = await page.$('body');
|
||||||
const element = await body.$('iframe >> control=enter-frame >> button');
|
const element = await body.$('iframe >> internal:control=enter-frame >> button');
|
||||||
expect(await element.textContent()).toBe('Hello iframe');
|
expect(await element.textContent()).toBe('Hello iframe');
|
||||||
const elements = await body.$$('iframe >> control=enter-frame >> span');
|
const elements = await body.$$('iframe >> internal:control=enter-frame >> span');
|
||||||
expect(elements).toHaveLength(2);
|
expect(elements).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work for $eval', async ({ page, server }) => {
|
it('should work for $eval', async ({ page, server }) => {
|
||||||
await routeIframe(page);
|
await routeIframe(page);
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
const value = await page.$eval('iframe >> control=enter-frame >> button', b => b.nodeName);
|
const value = await page.$eval('iframe >> internal:control=enter-frame >> button', b => b.nodeName);
|
||||||
expect(value).toBe('BUTTON');
|
expect(value).toBe('BUTTON');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -156,14 +156,14 @@ it('should work for $eval (handle)', async ({ page, server }) => {
|
|||||||
await routeIframe(page);
|
await routeIframe(page);
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
const body = await page.$('body');
|
const body = await page.$('body');
|
||||||
const value = await body.$eval('iframe >> control=enter-frame >> button', b => b.nodeName);
|
const value = await body.$eval('iframe >> internal:control=enter-frame >> button', b => b.nodeName);
|
||||||
expect(value).toBe('BUTTON');
|
expect(value).toBe('BUTTON');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work for $$eval', async ({ page, server }) => {
|
it('should work for $$eval', async ({ page, server }) => {
|
||||||
await routeIframe(page);
|
await routeIframe(page);
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
const value = await page.$$eval('iframe >> control=enter-frame >> span', ss => ss.map(s => s.textContent));
|
const value = await page.$$eval('iframe >> internal:control=enter-frame >> span', ss => ss.map(s => s.textContent));
|
||||||
expect(value).toEqual(['1', '2']);
|
expect(value).toEqual(['1', '2']);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -171,30 +171,30 @@ it('should work for $$eval (handle)', async ({ page, server }) => {
|
|||||||
await routeIframe(page);
|
await routeIframe(page);
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
const body = await page.$('body');
|
const body = await page.$('body');
|
||||||
const value = await body.$$eval('iframe >> control=enter-frame >> span', ss => ss.map(s => s.textContent));
|
const value = await body.$$eval('iframe >> internal:control=enter-frame >> span', ss => ss.map(s => s.textContent));
|
||||||
expect(value).toEqual(['1', '2']);
|
expect(value).toEqual(['1', '2']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not allow dangling enter-frame', async ({ page, server }) => {
|
it('should not allow dangling enter-frame', async ({ page, server }) => {
|
||||||
await routeIframe(page);
|
await routeIframe(page);
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
const button = page.locator('iframe >> control=enter-frame');
|
const button = page.locator('iframe >> internal:control=enter-frame');
|
||||||
const error = await button.click().catch(e => e);
|
const error = await button.click().catch(e => e);
|
||||||
expect(error.message).toContain('Selector cannot end with');
|
expect(error.message).toContain('Selector cannot end with');
|
||||||
expect(error.message).toContain('iframe >> control=enter-frame');
|
expect(error.message).toContain('iframe >> internal:control=enter-frame');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not allow leading enter-frame', async ({ page, server }) => {
|
it('should not allow leading enter-frame', async ({ page, server }) => {
|
||||||
await routeIframe(page);
|
await routeIframe(page);
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
const error = await page.waitForSelector('control=enter-frame >> button').catch(e => e);
|
const error = await page.waitForSelector('internal:control=enter-frame >> button').catch(e => e);
|
||||||
expect(error.message).toContain('Selector cannot start with');
|
expect(error.message).toContain('Selector cannot start with');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not allow capturing before enter-frame', async ({ page, server }) => {
|
it('should not allow capturing before enter-frame', async ({ page, server }) => {
|
||||||
await routeIframe(page);
|
await routeIframe(page);
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
const button = page.locator('*css=iframe >> control=enter-frame >> div');
|
const button = page.locator('*css=iframe >> internal:control=enter-frame >> div');
|
||||||
const error = await await button.click().catch(e => e);
|
const error = await await button.click().catch(e => e);
|
||||||
expect(error.message).toContain('Can not capture the selector before diving into the frame');
|
expect(error.message).toContain('Can not capture the selector before diving into the frame');
|
||||||
});
|
});
|
||||||
@ -202,7 +202,7 @@ it('should not allow capturing before enter-frame', async ({ page, server }) =>
|
|||||||
it('should capture after the enter-frame', async ({ page, server }) => {
|
it('should capture after the enter-frame', async ({ page, server }) => {
|
||||||
await routeIframe(page);
|
await routeIframe(page);
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
const div = page.locator('iframe >> control=enter-frame >> *css=div >> button');
|
const div = page.locator('iframe >> internal:control=enter-frame >> *css=div >> button');
|
||||||
expect(await div.innerHTML()).toContain('<button>');
|
expect(await div.innerHTML()).toContain('<button>');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -230,7 +230,7 @@ it('should click in lazy iframe', async ({ page, server }) => {
|
|||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
// Click in iframe
|
// Click in iframe
|
||||||
const button = page.locator('iframe >> control=enter-frame >> button');
|
const button = page.locator('iframe >> internal:control=enter-frame >> button');
|
||||||
const [, text] = await Promise.all([
|
const [, text] = await Promise.all([
|
||||||
button.click(),
|
button.click(),
|
||||||
button.innerText(),
|
button.innerText(),
|
||||||
@ -242,7 +242,7 @@ it('should click in lazy iframe', async ({ page, server }) => {
|
|||||||
it('waitFor should survive frame reattach', async ({ page, server }) => {
|
it('waitFor should survive frame reattach', async ({ page, server }) => {
|
||||||
await routeIframe(page);
|
await routeIframe(page);
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
const button = page.locator('iframe >> control=enter-frame >> button:has-text("Hello nested iframe")');
|
const button = page.locator('iframe >> internal:control=enter-frame >> button:has-text("Hello nested iframe")');
|
||||||
const promise = button.waitFor();
|
const promise = button.waitFor();
|
||||||
await page.locator('iframe').evaluate(e => e.remove());
|
await page.locator('iframe').evaluate(e => e.remove());
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
@ -257,7 +257,7 @@ it('waitForSelector should survive frame reattach (handle)', async ({ page, serv
|
|||||||
await routeIframe(page);
|
await routeIframe(page);
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
const body = await page.$('body');
|
const body = await page.$('body');
|
||||||
const promise = body.waitForSelector('iframe >> control=enter-frame >> button:has-text("Hello nested iframe")');
|
const promise = body.waitForSelector('iframe >> internal:control=enter-frame >> button:has-text("Hello nested iframe")');
|
||||||
await page.locator('iframe').evaluate(e => e.remove());
|
await page.locator('iframe').evaluate(e => e.remove());
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
const iframe = document.createElement('iframe');
|
const iframe = document.createElement('iframe');
|
||||||
@ -271,7 +271,7 @@ it('waitForSelector should survive iframe navigation (handle)', async ({ page, s
|
|||||||
await routeIframe(page);
|
await routeIframe(page);
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
const body = await page.$('body');
|
const body = await page.$('body');
|
||||||
const promise = body.waitForSelector('iframe >> control=enter-frame >> button:has-text("Hello nested iframe")');
|
const promise = body.waitForSelector('iframe >> internal:control=enter-frame >> button:has-text("Hello nested iframe")');
|
||||||
page.locator('iframe').evaluate(e => (e as HTMLIFrameElement).src = 'iframe-2.html');
|
page.locator('iframe').evaluate(e => (e as HTMLIFrameElement).src = 'iframe-2.html');
|
||||||
await promise;
|
await promise;
|
||||||
});
|
});
|
||||||
@ -279,7 +279,7 @@ it('waitForSelector should survive iframe navigation (handle)', async ({ page, s
|
|||||||
it('click should survive frame reattach', async ({ page, server }) => {
|
it('click should survive frame reattach', async ({ page, server }) => {
|
||||||
await routeIframe(page);
|
await routeIframe(page);
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
const button = page.locator('iframe >> control=enter-frame >> button:has-text("Hello nested iframe")');
|
const button = page.locator('iframe >> internal:control=enter-frame >> button:has-text("Hello nested iframe")');
|
||||||
const promise = button.click();
|
const promise = button.click();
|
||||||
await page.locator('iframe').evaluate(e => e.remove());
|
await page.locator('iframe').evaluate(e => e.remove());
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
@ -293,7 +293,7 @@ it('click should survive frame reattach', async ({ page, server }) => {
|
|||||||
it('click should survive iframe navigation', async ({ page, server }) => {
|
it('click should survive iframe navigation', async ({ page, server }) => {
|
||||||
await routeIframe(page);
|
await routeIframe(page);
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
const button = page.locator('iframe >> control=enter-frame >> button:has-text("Hello nested iframe")');
|
const button = page.locator('iframe >> internal:control=enter-frame >> button:has-text("Hello nested iframe")');
|
||||||
const promise = button.click();
|
const promise = button.click();
|
||||||
page.locator('iframe').evaluate(e => (e as HTMLIFrameElement).src = 'iframe-2.html');
|
page.locator('iframe').evaluate(e => (e as HTMLIFrameElement).src = 'iframe-2.html');
|
||||||
await promise;
|
await promise;
|
||||||
@ -322,7 +322,7 @@ it('should fail if element removed while waiting on element handle', async ({ pa
|
|||||||
it('should non work for non-frame', async ({ page, server }) => {
|
it('should non work for non-frame', async ({ page, server }) => {
|
||||||
await routeIframe(page);
|
await routeIframe(page);
|
||||||
await page.setContent('<div></div>');
|
await page.setContent('<div></div>');
|
||||||
const button = page.locator('div >> control=enter-frame >> button');
|
const button = page.locator('div >> internal:control=enter-frame >> button');
|
||||||
const error = await button.waitFor().catch(e => e);
|
const error = await button.waitFor().catch(e => e);
|
||||||
expect(error.message).toContain('<div></div>');
|
expect(error.message).toContain('<div></div>');
|
||||||
expect(error.message).toContain('<iframe> was expected');
|
expect(error.message).toContain('<iframe> was expected');
|
||||||
|
@ -32,6 +32,7 @@ it('getByText should work', async ({ page }) => {
|
|||||||
await page.setContent(`<div>yo</div><div>ya</div><div>\nye </div>`);
|
await page.setContent(`<div>yo</div><div>ya</div><div>\nye </div>`);
|
||||||
expect(await page.getByText('ye').evaluate(e => e.outerHTML)).toContain('>\nye </div>');
|
expect(await page.getByText('ye').evaluate(e => e.outerHTML)).toContain('>\nye </div>');
|
||||||
expect(await page.getByText(/ye/).evaluate(e => e.outerHTML)).toContain('>\nye </div>');
|
expect(await page.getByText(/ye/).evaluate(e => e.outerHTML)).toContain('>\nye </div>');
|
||||||
|
expect(await page.getByText(/e/).evaluate(e => e.outerHTML)).toContain('>\nye </div>');
|
||||||
|
|
||||||
await page.setContent(`<div> ye </div><div>ye</div>`);
|
await page.setContent(`<div> ye </div><div>ye</div>`);
|
||||||
expect(await page.getByText('ye', { exact: true }).first().evaluate(e => e.outerHTML)).toContain('> ye </div>');
|
expect(await page.getByText('ye', { exact: true }).first().evaluate(e => e.outerHTML)).toContain('> ye </div>');
|
||||||
@ -48,6 +49,22 @@ it('getByLabel should work', async ({ page }) => {
|
|||||||
expect(await page.locator('div').getByLabel('Name').evaluate(e => e.nodeName)).toBe('INPUT');
|
expect(await page.locator('div').getByLabel('Name').evaluate(e => e.nodeName)).toBe('INPUT');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('getByLabel should work with nested elements', async ({ page }) => {
|
||||||
|
await page.setContent(`<label for=target>Last <span>Name</span></label><input id=target type=text>`);
|
||||||
|
|
||||||
|
await expect(page.getByLabel('last name')).toHaveAttribute('id', 'target');
|
||||||
|
await expect(page.getByLabel('st na')).toHaveAttribute('id', 'target');
|
||||||
|
await expect(page.getByLabel('Name')).toHaveAttribute('id', 'target');
|
||||||
|
await expect(page.getByLabel('Last Name', { exact: true })).toHaveAttribute('id', 'target');
|
||||||
|
await expect(page.getByLabel(/Last\s+name/i)).toHaveAttribute('id', 'target');
|
||||||
|
|
||||||
|
expect(await page.getByLabel('Last', { exact: true }).elementHandles()).toEqual([]);
|
||||||
|
expect(await page.getByLabel('last name', { exact: true }).elementHandles()).toEqual([]);
|
||||||
|
expect(await page.getByLabel('Name', { exact: true }).elementHandles()).toEqual([]);
|
||||||
|
expect(await page.getByLabel('what?').elementHandles()).toEqual([]);
|
||||||
|
expect(await page.getByLabel(/last name/).elementHandles()).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
it('getByPlaceholder should work', async ({ page }) => {
|
it('getByPlaceholder should work', async ({ page }) => {
|
||||||
await page.setContent(`<div>
|
await page.setContent(`<div>
|
||||||
<input placeholder='Hello'>
|
<input placeholder='Hello'>
|
||||||
@ -89,3 +106,31 @@ it('getByTitle should work', async ({ page }) => {
|
|||||||
await expect(page.mainFrame().getByTitle('hello')).toHaveCount(2);
|
await expect(page.mainFrame().getByTitle('hello')).toHaveCount(2);
|
||||||
await expect(page.locator('div').getByTitle('hello')).toHaveCount(2);
|
await expect(page.locator('div').getByTitle('hello')).toHaveCount(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('getBy escaping', async ({ page }) => {
|
||||||
|
await page.setContent(`<label id=label for=control>Hello
|
||||||
|
wo"rld</label><input id=control />`);
|
||||||
|
await page.$eval('input', input => {
|
||||||
|
input.setAttribute('placeholder', 'hello\nwo"rld');
|
||||||
|
input.setAttribute('title', 'hello\nwo"rld');
|
||||||
|
input.setAttribute('alt', 'hello\nwo"rld');
|
||||||
|
});
|
||||||
|
await expect(page.getByText('hello\nwo"rld')).toHaveAttribute('id', 'label');
|
||||||
|
await expect(page.getByLabel('hello\nwo"rld')).toHaveAttribute('id', 'control');
|
||||||
|
await expect(page.getByPlaceholder('hello\nwo"rld')).toHaveAttribute('id', 'control');
|
||||||
|
await expect(page.getByAltText('hello\nwo"rld')).toHaveAttribute('id', 'control');
|
||||||
|
await expect(page.getByTitle('hello\nwo"rld')).toHaveAttribute('id', 'control');
|
||||||
|
|
||||||
|
await page.setContent(`<label id=label for=control>Hello
|
||||||
|
world</label><input id=control />`);
|
||||||
|
await page.$eval('input', input => {
|
||||||
|
input.setAttribute('placeholder', 'hello\nworld');
|
||||||
|
input.setAttribute('title', 'hello\nworld');
|
||||||
|
input.setAttribute('alt', 'hello\nworld');
|
||||||
|
});
|
||||||
|
await expect(page.getByText('hello\nworld')).toHaveAttribute('id', 'label');
|
||||||
|
await expect(page.getByLabel('hello\nworld')).toHaveAttribute('id', 'control');
|
||||||
|
await expect(page.getByPlaceholder('hello\nworld')).toHaveAttribute('id', 'control');
|
||||||
|
await expect(page.getByAltText('hello\nworld')).toHaveAttribute('id', 'control');
|
||||||
|
await expect(page.getByTitle('hello\nworld')).toHaveAttribute('id', 'control');
|
||||||
|
});
|
||||||
|
@ -368,37 +368,37 @@ it('should properly determine visibility of display:contents elements', async ({
|
|||||||
await page.waitForSelector('article', { state: 'hidden' });
|
await page.waitForSelector('article', { state: 'hidden' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work with has=', async ({ page, server }) => {
|
it('should work with internal:has=', async ({ page, server }) => {
|
||||||
await page.goto(server.PREFIX + '/deep-shadow.html');
|
await page.goto(server.PREFIX + '/deep-shadow.html');
|
||||||
expect(await page.$$eval(`div >> has="#target"`, els => els.length)).toBe(2);
|
expect(await page.$$eval(`div >> internal:has="#target"`, els => els.length)).toBe(2);
|
||||||
expect(await page.$$eval(`div >> has="[data-testid=foo]"`, els => els.length)).toBe(3);
|
expect(await page.$$eval(`div >> internal:has="[data-testid=foo]"`, els => els.length)).toBe(3);
|
||||||
expect(await page.$$eval(`div >> has="[attr*=value]"`, els => els.length)).toBe(2);
|
expect(await page.$$eval(`div >> internal:has="[attr*=value]"`, els => els.length)).toBe(2);
|
||||||
|
|
||||||
await page.setContent(`<section><span></span><div></div></section><section><br></section>`);
|
await page.setContent(`<section><span></span><div></div></section><section><br></section>`);
|
||||||
expect(await page.$$eval(`section >> has="span, div"`, els => els.length)).toBe(1);
|
expect(await page.$$eval(`section >> internal:has="span, div"`, els => els.length)).toBe(1);
|
||||||
expect(await page.$$eval(`section >> has="span, div"`, els => els.length)).toBe(1);
|
expect(await page.$$eval(`section >> internal:has="span, div"`, els => els.length)).toBe(1);
|
||||||
expect(await page.$$eval(`section >> has="br"`, els => els.length)).toBe(1);
|
expect(await page.$$eval(`section >> internal:has="br"`, els => els.length)).toBe(1);
|
||||||
expect(await page.$$eval(`section >> has="span, br"`, els => els.length)).toBe(2);
|
expect(await page.$$eval(`section >> internal:has="span, br"`, els => els.length)).toBe(2);
|
||||||
expect(await page.$$eval(`section >> has="span, br, div"`, els => els.length)).toBe(2);
|
expect(await page.$$eval(`section >> internal:has="span, br, div"`, els => els.length)).toBe(2);
|
||||||
|
|
||||||
await page.setContent(`<div><span>hello</span></div><div><span>world</span></div>`);
|
await page.setContent(`<div><span>hello</span></div><div><span>world</span></div>`);
|
||||||
expect(await page.$$eval(`div >> has="text=world"`, els => els.length)).toBe(1);
|
expect(await page.$$eval(`div >> internal:has="text=world"`, els => els.length)).toBe(1);
|
||||||
expect(await page.$eval(`div >> has="text=world"`, e => e.outerHTML)).toBe(`<div><span>world</span></div>`);
|
expect(await page.$eval(`div >> internal:has="text=world"`, e => e.outerHTML)).toBe(`<div><span>world</span></div>`);
|
||||||
expect(await page.$$eval(`div >> has="text=\\"hello\\""`, els => els.length)).toBe(1);
|
expect(await page.$$eval(`div >> internal:has="text=\\"hello\\""`, els => els.length)).toBe(1);
|
||||||
expect(await page.$eval(`div >> has="text=\\"hello\\""`, e => e.outerHTML)).toBe(`<div><span>hello</span></div>`);
|
expect(await page.$eval(`div >> internal:has="text=\\"hello\\""`, e => e.outerHTML)).toBe(`<div><span>hello</span></div>`);
|
||||||
expect(await page.$$eval(`div >> has="xpath=./span"`, els => els.length)).toBe(2);
|
expect(await page.$$eval(`div >> internal:has="xpath=./span"`, els => els.length)).toBe(2);
|
||||||
expect(await page.$$eval(`div >> has="span"`, els => els.length)).toBe(2);
|
expect(await page.$$eval(`div >> internal:has="span"`, els => els.length)).toBe(2);
|
||||||
expect(await page.$$eval(`div >> has="span >> text=wor"`, els => els.length)).toBe(1);
|
expect(await page.$$eval(`div >> internal:has="span >> text=wor"`, els => els.length)).toBe(1);
|
||||||
expect(await page.$eval(`div >> has="span >> text=wor"`, e => e.outerHTML)).toBe(`<div><span>world</span></div>`);
|
expect(await page.$eval(`div >> internal:has="span >> text=wor"`, e => e.outerHTML)).toBe(`<div><span>world</span></div>`);
|
||||||
expect(await page.$eval(`div >> has="span >> text=wor" >> span`, e => e.outerHTML)).toBe(`<span>world</span>`);
|
expect(await page.$eval(`div >> internal:has="span >> text=wor" >> span`, e => e.outerHTML)).toBe(`<span>world</span>`);
|
||||||
|
|
||||||
const error1 = await page.$(`div >> has=abc`).catch(e => e);
|
const error1 = await page.$(`div >> internal:has=abc`).catch(e => e);
|
||||||
expect(error1.message).toContain('Malformed selector: has=abc');
|
expect(error1.message).toContain('Malformed selector: internal:has=abc');
|
||||||
const error2 = await page.$(`has="div"`).catch(e => e);
|
const error2 = await page.$(`internal:has="div"`).catch(e => e);
|
||||||
expect(error2.message).toContain('"has" selector cannot be first');
|
expect(error2.message).toContain('"internal:has" selector cannot be first');
|
||||||
const error3 = await page.$(`div >> has=33`).catch(e => e);
|
const error3 = await page.$(`div >> internal:has=33`).catch(e => e);
|
||||||
expect(error3.message).toContain('Malformed selector: has=33');
|
expect(error3.message).toContain('Malformed selector: internal:has=33');
|
||||||
const error4 = await page.$(`div >> has="span!"`).catch(e => e);
|
const error4 = await page.$(`div >> internal:has="span!"`).catch(e => e);
|
||||||
expect(error4.message).toContain('Unexpected token "!" while parsing selector "span!"');
|
expect(error4.message).toContain('Unexpected token "!" while parsing selector "span!"');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user