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:
Dmitry Gozman 2022-10-05 08:45:10 -07:00 committed by GitHub
parent f64dbe9ece
commit 2bcd9ce9ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 170 additions and 131 deletions

View File

@ -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 {

View File

@ -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);
}); });
} }

View File

@ -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);

View File

@ -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 {

View File

@ -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 });

View File

@ -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) => {

View File

@ -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);

View File

@ -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') {

View File

@ -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',

View File

@ -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;
} }

View File

@ -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;
} }

View File

@ -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'),

View File

@ -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&gt;&gt;text</div>`); await page.setContent(`<div>text&gt;&gt;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"/>`);

View File

@ -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');
});

View File

@ -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');

View File

@ -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');
});

View File

@ -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!"');
}); });