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 {
|
||||
if (!isString(text))
|
||||
return `attr=[${attrName}=${text}]`;
|
||||
return `attr=[${attrName}=${escapeForAttributeSelector(text)}${options?.exact ? 's' : 'i'}]`;
|
||||
return `internal:attr=[${attrName}=${text}]`;
|
||||
return `internal:attr=[${attrName}=${escapeForAttributeSelector(text)}${options?.exact ? 's' : 'i'}]`;
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -108,14 +108,14 @@ export class Locator implements api.Locator {
|
||||
|
||||
if (options?.hasText) {
|
||||
const textSelector = 'text=' + escapeForTextSelector(options.hasText, false);
|
||||
this._selector += ` >> has=${JSON.stringify(textSelector)}`;
|
||||
this._selector += ` >> internal:has=${JSON.stringify(textSelector)}`;
|
||||
}
|
||||
|
||||
if (options?.has) {
|
||||
const locator = options.has;
|
||||
if (locator._frame !== 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 {
|
||||
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 {
|
||||
@ -427,7 +427,7 @@ export class FrameLocator implements api.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 {
|
||||
|
@ -1501,7 +1501,7 @@ export class Frame extends SdkObject {
|
||||
return this.retryWithProgress(progress, selector, options, async selectorInFrame => {
|
||||
// Be careful, |this| can be different from |frame|.
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
@ -28,10 +28,10 @@ function createLocator(injectedScript: InjectedScript, initial: string, options?
|
||||
this.selector = selector;
|
||||
if (options?.hasText) {
|
||||
const textSelector = 'text=' + escapeForTextSelector(options.hasText, false);
|
||||
this.selector += ` >> has=${JSON.stringify(textSelector)}`;
|
||||
this.selector += ` >> internal:has=${JSON.stringify(textSelector)}`;
|
||||
}
|
||||
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);
|
||||
this.element = injectedScript.querySelector(parsed, document, false);
|
||||
this.elements = injectedScript.querySelectorAll(parsed, document);
|
||||
|
@ -22,7 +22,7 @@ import { RoleEngine } from './roleSelectorEngine';
|
||||
import { parseAttributeSelector } from '../isomorphic/selectorParser';
|
||||
import type { NestedSelectorBody, ParsedSelector, ParsedSelectorPart } 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 { enclosingShadowRootOrDocument, isElementVisible, parentElementOrShadowHost } from './domUtils';
|
||||
import type { CSSComplexSelectorList } from '../isomorphic/cssParser';
|
||||
@ -103,9 +103,10 @@ export class InjectedScript {
|
||||
this._engines.set('css', this._createCSSEngine());
|
||||
this._engines.set('nth', { queryAll: () => [] });
|
||||
this._engines.set('visible', this._createVisibleEngine());
|
||||
this._engines.set('control', this._createControlEngine());
|
||||
this._engines.set('has', this._createHasEngine());
|
||||
this._engines.set('attr', this._createNamedAttributeEngine());
|
||||
this._engines.set('internal:control', this._createControlEngine());
|
||||
this._engines.set('internal:has', this._createHasEngine());
|
||||
this._engines.set('internal:label', this._createLabelEngine());
|
||||
this._engines.set('internal:attr', this._createNamedAttributeEngine());
|
||||
|
||||
for (const { name, engine } of customEngines)
|
||||
this._engines.set(name, engine);
|
||||
@ -173,7 +174,7 @@ export class InjectedScript {
|
||||
const withHas: ParsedSelector = { parts: selector.parts.slice(0, selector.capture + 1) };
|
||||
if (selector.capture < selector.parts.length - 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);
|
||||
}
|
||||
return this.querySelectorAll(withHas, root);
|
||||
@ -243,7 +244,7 @@ export class InjectedScript {
|
||||
|
||||
private _createTextEngine(shadow: boolean): SelectorEngine {
|
||||
const queryList = (root: SelectorRoot, selector: string): Element[] => {
|
||||
const { matcher, kind } = createTextMatcher(selector);
|
||||
const { matcher, kind } = createTextMatcher(selector, false);
|
||||
const result: Element[] = [];
|
||||
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 {
|
||||
const queryList = (root: SelectorRoot, selector: string): Element[] => {
|
||||
const parsed = parseAttributeSelector(selector, true);
|
||||
@ -305,10 +323,6 @@ export class InjectedScript {
|
||||
return [];
|
||||
if (body === 'return-empty')
|
||||
return [];
|
||||
if (body === 'resolve-label') {
|
||||
const control = (root as HTMLLabelElement).control;
|
||||
return control ? [control] : [];
|
||||
}
|
||||
if (body === 'component') {
|
||||
if (root.nodeType !== 1 /* Node.ELEMENT_NODE */)
|
||||
return [];
|
||||
@ -316,7 +330,7 @@ export class InjectedScript {
|
||||
// However, when mounting fragments, return the root instead.
|
||||
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('');
|
||||
}
|
||||
|
||||
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) {
|
||||
const lastSlash = selector.lastIndexOf('/');
|
||||
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));
|
||||
strict = true;
|
||||
}
|
||||
const matcher = strict ? createStrictTextMatcher(selector) : createLaxTextMatcher(selector);
|
||||
return { matcher, kind: strict ? 'strict' : 'lax' };
|
||||
if (strict)
|
||||
return { matcher: strictMatchesFullText ? createStrictFullTextMatcher(selector) : createStrictTextMatcher(selector), kind: 'strict' };
|
||||
return { matcher: createLaxTextMatcher(selector), kind: 'lax' };
|
||||
}
|
||||
|
||||
class ExpectedTextMatcher {
|
||||
|
@ -148,7 +148,7 @@ function buildCandidates(element: Element, accessibleNameCache: Map<Element, boo
|
||||
const candidates: SelectorToken[] = [];
|
||||
|
||||
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']) {
|
||||
if (element.getAttribute(attr))
|
||||
@ -158,7 +158,7 @@ function buildCandidates(element: Element, accessibleNameCache: Map<Element, boo
|
||||
if (element.nodeName === 'INPUT') {
|
||||
const input = element as HTMLInputElement;
|
||||
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);
|
||||
@ -171,7 +171,7 @@ function buildCandidates(element: Element, accessibleNameCache: Map<Element, boo
|
||||
}
|
||||
|
||||
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))
|
||||
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 {
|
||||
const re = new RegExp(source, flags);
|
||||
return (elementText: ElementText) => {
|
||||
|
@ -19,7 +19,7 @@ import { InvalidSelectorError, parseCSS } from './cssParser';
|
||||
export { InvalidSelectorError, isInvalidSelectorError } from './cssParser';
|
||||
|
||||
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']);
|
||||
|
||||
export type ParsedSelectorPart = {
|
||||
@ -70,7 +70,7 @@ export function parseSelector(selector: string): ParsedSelector {
|
||||
throw new Error(`Malformed selector: ${part.name}=` + part.body);
|
||||
}
|
||||
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`);
|
||||
return result;
|
||||
}
|
||||
@ -93,7 +93,7 @@ export function splitSelectorByFrame(selectorText: string): ParsedSelector[] {
|
||||
let chunkStartIndex = 0;
|
||||
for (let i = 0; i < selector.parts.length; ++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)
|
||||
throw new InvalidSelectorError('Selector cannot start with entering frame, select the iframe first');
|
||||
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 { name, value } = attrSelector.attributes[0];
|
||||
if (name === 'data-testid') {
|
||||
|
@ -45,8 +45,8 @@ export class Selectors {
|
||||
'data-testid', 'data-testid:light',
|
||||
'data-test-id', 'data-test-id:light',
|
||||
'data-test', 'data-test:light',
|
||||
'nth', 'visible', 'control', 'has',
|
||||
'role', 'attr'
|
||||
'nth', 'visible', 'internal:control', 'internal:has',
|
||||
'role', 'internal:attr', 'internal:label'
|
||||
]);
|
||||
this._builtinEnginesInMainWorld = new Set([
|
||||
'_react', '_vue',
|
||||
|
@ -64,7 +64,7 @@ export function escapeForTextSelector(text: string | RegExp, exact: boolean, cas
|
||||
if (exact)
|
||||
return '"' + text.replace(/["]/g, '\\"') + '"';
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -124,7 +124,7 @@ async function innerMount(page: Page, jsxOrType: JsxComponent | string, options:
|
||||
|
||||
await window.playwrightMount(component, rootElement, hooksConfig);
|
||||
|
||||
return '#root >> control=component';
|
||||
return '#root >> internal:control=component';
|
||||
}, { component, hooksConfig: options.hooksConfig });
|
||||
return selector;
|
||||
}
|
||||
|
@ -235,7 +235,7 @@ test.describe('cli codegen', () => {
|
||||
await recorder.setContentAndWait(`<div data-testid=testid onclick="console.log('click')">Submit</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([
|
||||
page.waitForEvent('console', msg => msg.type() !== 'error'),
|
||||
@ -267,7 +267,7 @@ test.describe('cli codegen', () => {
|
||||
await recorder.setContentAndWait(`<input placeholder="Country"></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([
|
||||
recorder.waitForOutput('JavaScript', 'click'),
|
||||
@ -296,7 +296,7 @@ test.describe('cli codegen', () => {
|
||||
await recorder.setContentAndWait(`<input alt="Country"></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([
|
||||
recorder.waitForOutput('JavaScript', 'click'),
|
||||
|
@ -45,7 +45,7 @@ it.describe('selector generator', () => {
|
||||
|
||||
it('should not escape spaces inside named attr selectors', async ({ page }) => {
|
||||
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 }) => {
|
||||
@ -60,17 +60,17 @@ it.describe('selector generator', () => {
|
||||
|
||||
it('should escape text with >>', async ({ page }) => {
|
||||
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 }) => {
|
||||
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 }) => {
|
||||
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 }) => {
|
||||
@ -88,7 +88,7 @@ it.describe('selector generator', () => {
|
||||
|
||||
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>`);
|
||||
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 }) => {
|
||||
@ -99,7 +99,7 @@ it.describe('selector generator', () => {
|
||||
<div data-testid=a>
|
||||
Text
|
||||
</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 }) => {
|
||||
@ -110,7 +110,7 @@ it.describe('selector generator', () => {
|
||||
<div data-testid=a mark=1>
|
||||
Text
|
||||
</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 }) => {
|
||||
@ -232,7 +232,7 @@ it.describe('selector generator', () => {
|
||||
});
|
||||
it('placeholder', async ({ page }) => {
|
||||
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 }) => {
|
||||
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.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 }) => {
|
||||
await routeIframe(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();
|
||||
expect(await button.innerText()).toBe('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 page.goto(server.EMPTY_PAGE);
|
||||
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.textContent()).toBe('Hello iframe');
|
||||
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 }) => {
|
||||
await routeIframe(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();
|
||||
expect(await button.innerText()).toBe('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 page.goto(server.EMPTY_PAGE);
|
||||
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.textContent()).toBe('Hello nested iframe');
|
||||
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 }) => {
|
||||
await routeIframe(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');
|
||||
const elements = await page.$$('iframe >> control=enter-frame >> span');
|
||||
const elements = await page.$$('iframe >> internal:control=enter-frame >> span');
|
||||
expect(elements).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('$ should not wait for frame', async ({ page, server }) => {
|
||||
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');
|
||||
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 }) => {
|
||||
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');
|
||||
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 }) => {
|
||||
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');
|
||||
}
|
||||
{
|
||||
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');
|
||||
}
|
||||
});
|
||||
@ -125,12 +125,12 @@ 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);
|
||||
{
|
||||
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');
|
||||
}
|
||||
{
|
||||
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');
|
||||
}
|
||||
});
|
||||
@ -139,16 +139,16 @@ it('should work for $ and $$ (handle)', async ({ page, server }) => {
|
||||
await routeIframe(page);
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
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');
|
||||
const elements = await body.$$('iframe >> control=enter-frame >> span');
|
||||
const elements = await body.$$('iframe >> internal:control=enter-frame >> span');
|
||||
expect(elements).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should work for $eval', async ({ page, server }) => {
|
||||
await routeIframe(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');
|
||||
});
|
||||
|
||||
@ -156,14 +156,14 @@ it('should work for $eval (handle)', async ({ page, server }) => {
|
||||
await routeIframe(page);
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
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');
|
||||
});
|
||||
|
||||
it('should work for $$eval', async ({ page, server }) => {
|
||||
await routeIframe(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']);
|
||||
});
|
||||
|
||||
@ -171,30 +171,30 @@ it('should work for $$eval (handle)', async ({ page, server }) => {
|
||||
await routeIframe(page);
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
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']);
|
||||
});
|
||||
|
||||
it('should not allow dangling enter-frame', async ({ page, server }) => {
|
||||
await routeIframe(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);
|
||||
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 }) => {
|
||||
await routeIframe(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');
|
||||
});
|
||||
|
||||
it('should not allow capturing before enter-frame', async ({ page, server }) => {
|
||||
await routeIframe(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);
|
||||
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 }) => {
|
||||
await routeIframe(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>');
|
||||
});
|
||||
|
||||
@ -230,7 +230,7 @@ it('should click in lazy iframe', async ({ page, server }) => {
|
||||
}, 500);
|
||||
|
||||
// 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([
|
||||
button.click(),
|
||||
button.innerText(),
|
||||
@ -242,7 +242,7 @@ it('should click in lazy iframe', async ({ page, server }) => {
|
||||
it('waitFor should survive frame reattach', async ({ page, server }) => {
|
||||
await routeIframe(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();
|
||||
await page.locator('iframe').evaluate(e => e.remove());
|
||||
await page.evaluate(() => {
|
||||
@ -257,7 +257,7 @@ it('waitForSelector should survive frame reattach (handle)', async ({ page, serv
|
||||
await routeIframe(page);
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
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.evaluate(() => {
|
||||
const iframe = document.createElement('iframe');
|
||||
@ -271,7 +271,7 @@ it('waitForSelector should survive iframe navigation (handle)', async ({ page, s
|
||||
await routeIframe(page);
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
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');
|
||||
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 }) => {
|
||||
await routeIframe(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();
|
||||
await page.locator('iframe').evaluate(e => e.remove());
|
||||
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 }) => {
|
||||
await routeIframe(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();
|
||||
page.locator('iframe').evaluate(e => (e as HTMLIFrameElement).src = 'iframe-2.html');
|
||||
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 }) => {
|
||||
await routeIframe(page);
|
||||
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);
|
||||
expect(error.message).toContain('<div></div>');
|
||||
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>`);
|
||||
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>`);
|
||||
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');
|
||||
});
|
||||
|
||||
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 }) => {
|
||||
await page.setContent(`<div>
|
||||
<input placeholder='Hello'>
|
||||
@ -89,3 +106,31 @@ it('getByTitle should work', async ({ page }) => {
|
||||
await expect(page.mainFrame().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' });
|
||||
});
|
||||
|
||||
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');
|
||||
expect(await page.$$eval(`div >> 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 >> has="[attr*=value]"`, els => els.length)).toBe(2);
|
||||
expect(await page.$$eval(`div >> internal:has="#target"`, els => els.length)).toBe(2);
|
||||
expect(await page.$$eval(`div >> internal:has="[data-testid=foo]"`, els => els.length)).toBe(3);
|
||||
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>`);
|
||||
expect(await page.$$eval(`section >> 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 >> 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 >> has="span, br, div"`, els => els.length)).toBe(2);
|
||||
expect(await page.$$eval(`section >> internal: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 >> internal:has="br"`, els => els.length)).toBe(1);
|
||||
expect(await page.$$eval(`section >> internal:has="span, br"`, 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>`);
|
||||
expect(await page.$$eval(`div >> 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 >> 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 >> 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 >> 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 >> has="span >> text=wor" >> span`, e => e.outerHTML)).toBe(`<span>world</span>`);
|
||||
expect(await page.$$eval(`div >> internal:has="text=world"`, els => els.length)).toBe(1);
|
||||
expect(await page.$eval(`div >> internal:has="text=world"`, e => e.outerHTML)).toBe(`<div><span>world</span></div>`);
|
||||
expect(await page.$$eval(`div >> internal:has="text=\\"hello\\""`, els => els.length)).toBe(1);
|
||||
expect(await page.$eval(`div >> internal:has="text=\\"hello\\""`, e => e.outerHTML)).toBe(`<div><span>hello</span></div>`);
|
||||
expect(await page.$$eval(`div >> internal:has="xpath=./span"`, els => els.length)).toBe(2);
|
||||
expect(await page.$$eval(`div >> internal:has="span"`, els => els.length)).toBe(2);
|
||||
expect(await page.$$eval(`div >> internal:has="span >> text=wor"`, els => els.length)).toBe(1);
|
||||
expect(await page.$eval(`div >> internal:has="span >> text=wor"`, e => e.outerHTML)).toBe(`<div><span>world</span></div>`);
|
||||
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);
|
||||
expect(error1.message).toContain('Malformed selector: has=abc');
|
||||
const error2 = await page.$(`has="div"`).catch(e => e);
|
||||
expect(error2.message).toContain('"has" selector cannot be first');
|
||||
const error3 = await page.$(`div >> has=33`).catch(e => e);
|
||||
expect(error3.message).toContain('Malformed selector: has=33');
|
||||
const error4 = await page.$(`div >> has="span!"`).catch(e => e);
|
||||
const error1 = await page.$(`div >> internal:has=abc`).catch(e => e);
|
||||
expect(error1.message).toContain('Malformed selector: internal:has=abc');
|
||||
const error2 = await page.$(`internal:has="div"`).catch(e => e);
|
||||
expect(error2.message).toContain('"internal:has" selector cannot be first');
|
||||
const error3 = await page.$(`div >> internal:has=33`).catch(e => e);
|
||||
expect(error3.message).toContain('Malformed selector: internal:has=33');
|
||||
const error4 = await page.$(`div >> internal:has="span!"`).catch(e => e);
|
||||
expect(error4.message).toContain('Unexpected token "!" while parsing selector "span!"');
|
||||
});
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user