mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-13 17:14:02 +03:00
feat(selectors): support various kinds of selectors (#118)
This adds support for generic "engine=body [>> engine=body]*" selector syntax and auto-detects simple css or xpath.
This commit is contained in:
parent
505c9e3660
commit
bb1433a143
165
src/dom.ts
165
src/dom.ts
@ -25,25 +25,29 @@ export interface DOMWorldDelegate {
|
||||
adoptElementHandle(handle: ElementHandle, to: DOMWorld): Promise<ElementHandle>;
|
||||
}
|
||||
|
||||
type SelectorRoot = Element | ShadowRoot | Document;
|
||||
|
||||
type ResolvedSelector = { root?: ElementHandle, selector: string, disposeRoot?: boolean };
|
||||
type Selector = string | { root?: ElementHandle, selector: string };
|
||||
|
||||
export class DOMWorld {
|
||||
readonly context: js.ExecutionContext;
|
||||
readonly delegate: DOMWorldDelegate;
|
||||
|
||||
private _injectedPromise?: Promise<js.JSHandle>;
|
||||
private _documentPromise?: Promise<ElementHandle>;
|
||||
|
||||
constructor(context: js.ExecutionContext, delegate: DOMWorldDelegate) {
|
||||
this.context = context;
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
_createHandle(remoteObject: any): ElementHandle | null {
|
||||
createHandle(remoteObject: any): ElementHandle | null {
|
||||
if (this.delegate.isElement(remoteObject))
|
||||
return new ElementHandle(this.context, remoteObject);
|
||||
return null;
|
||||
}
|
||||
|
||||
injected(): Promise<js.JSHandle> {
|
||||
private _injected(): Promise<js.JSHandle> {
|
||||
if (!this._injectedPromise) {
|
||||
const engineSources = [cssSelectorEngineSource.source, xpathSelectorEngineSource.source];
|
||||
const source = `
|
||||
@ -56,24 +60,91 @@ export class DOMWorld {
|
||||
return this._injectedPromise;
|
||||
}
|
||||
|
||||
_document(): Promise<ElementHandle> {
|
||||
if (!this._documentPromise)
|
||||
this._documentPromise = this.context.evaluateHandle('document').then(handle => handle.asElement()!);
|
||||
return this._documentPromise;
|
||||
async adoptElementHandle(handle: ElementHandle): Promise<ElementHandle> {
|
||||
assert(handle.executionContext() !== this.context, 'Should not adopt to the same context');
|
||||
return this.delegate.adoptElementHandle(handle, this);
|
||||
}
|
||||
|
||||
async adoptElementHandle(handle: ElementHandle, dispose: boolean): Promise<ElementHandle> {
|
||||
if (handle.executionContext() === this.context)
|
||||
return handle;
|
||||
const adopted = this.delegate.adoptElementHandle(handle, this);
|
||||
if (dispose)
|
||||
private _normalizeSelector(selector: string): string {
|
||||
const eqIndex = selector.indexOf('=');
|
||||
if (eqIndex !== -1 && selector.substring(0, eqIndex).trim().match(/^[a-zA-Z_0-9]+$/))
|
||||
return selector;
|
||||
if (selector.startsWith('//'))
|
||||
return 'xpath=' + selector;
|
||||
return 'css=' + selector;
|
||||
}
|
||||
|
||||
private async _resolveSelector(selector: Selector): Promise<ResolvedSelector> {
|
||||
if (helper.isString(selector))
|
||||
return { selector: this._normalizeSelector(selector) };
|
||||
if (selector.root && selector.root.executionContext() !== this.context) {
|
||||
const root = await this.adoptElementHandle(selector.root);
|
||||
return { root, selector: this._normalizeSelector(selector.selector), disposeRoot: true };
|
||||
}
|
||||
return { root: selector.root, selector: this._normalizeSelector(selector.selector) };
|
||||
}
|
||||
|
||||
private _selectorToString(selector: Selector): string {
|
||||
if (typeof selector === 'string')
|
||||
return selector;
|
||||
return `:scope >> ${selector.selector}`;
|
||||
}
|
||||
|
||||
async $(selector: Selector): Promise<ElementHandle | null> {
|
||||
const resolved = await this._resolveSelector(selector);
|
||||
const handle = await this.context.evaluateHandle(
|
||||
(injected: Injected, selector: string, root: SelectorRoot | undefined) => injected.querySelector(selector, root || document),
|
||||
await this._injected(), resolved.selector, resolved.root
|
||||
);
|
||||
if (resolved.disposeRoot)
|
||||
await resolved.root.dispose();
|
||||
if (!handle.asElement())
|
||||
await handle.dispose();
|
||||
return adopted;
|
||||
return handle.asElement();
|
||||
}
|
||||
|
||||
async $$(selector: Selector): Promise<ElementHandle[]> {
|
||||
const resolved = await this._resolveSelector(selector);
|
||||
const arrayHandle = await this.context.evaluateHandle(
|
||||
(injected: Injected, selector: string, root: SelectorRoot | undefined) => injected.querySelectorAll(selector, root || document),
|
||||
await this._injected(), resolved.selector, resolved.root
|
||||
);
|
||||
if (resolved.disposeRoot)
|
||||
await resolved.root.dispose();
|
||||
const properties = await arrayHandle.getProperties();
|
||||
await arrayHandle.dispose();
|
||||
const result = [];
|
||||
for (const property of properties.values()) {
|
||||
const elementHandle = property.asElement();
|
||||
if (elementHandle)
|
||||
result.push(elementHandle);
|
||||
else
|
||||
await property.dispose();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
$eval: types.$Eval<Selector> = async (selector, pageFunction, ...args) => {
|
||||
const elementHandle = await this.$(selector);
|
||||
if (!elementHandle)
|
||||
throw new Error(`Error: failed to find element matching selector "${this._selectorToString(selector)}"`);
|
||||
const result = await elementHandle.evaluate(pageFunction, ...args as any);
|
||||
await elementHandle.dispose();
|
||||
return result;
|
||||
}
|
||||
|
||||
$$eval: types.$$Eval<Selector> = async (selector, pageFunction, ...args) => {
|
||||
const resolved = await this._resolveSelector(selector);
|
||||
const arrayHandle = await this.context.evaluateHandle(
|
||||
(injected: Injected, selector: string, root: SelectorRoot | undefined) => injected.querySelectorAll(selector, root || document),
|
||||
await this._injected(), resolved.selector, resolved.root
|
||||
);
|
||||
const result = await arrayHandle.evaluate(pageFunction, ...args as any);
|
||||
await arrayHandle.dispose();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
type SelectorRoot = Element | ShadowRoot | Document;
|
||||
|
||||
export class ElementHandle extends js.JSHandle {
|
||||
private readonly _world: DOMWorld;
|
||||
|
||||
@ -198,68 +269,24 @@ export class ElementHandle extends js.JSHandle {
|
||||
return this._world.delegate.screenshot(this, options);
|
||||
}
|
||||
|
||||
async $(selector: string): Promise<ElementHandle | null> {
|
||||
const handle = await this.evaluateHandle(
|
||||
(root: SelectorRoot, selector: string, injected: Injected) => injected.querySelector('css=' + selector, root),
|
||||
selector, await this._world.injected()
|
||||
);
|
||||
const element = handle.asElement();
|
||||
if (element)
|
||||
return element;
|
||||
await handle.dispose();
|
||||
return null;
|
||||
$(selector: string): Promise<ElementHandle | null> {
|
||||
return this._world.$({ root: this, selector });
|
||||
}
|
||||
|
||||
async $$(selector: string): Promise<ElementHandle[]> {
|
||||
const arrayHandle = await this.evaluateHandle(
|
||||
(root: SelectorRoot, selector: string, injected: Injected) => injected.querySelectorAll('css=' + selector, root),
|
||||
selector, await this._world.injected()
|
||||
);
|
||||
const properties = await arrayHandle.getProperties();
|
||||
await arrayHandle.dispose();
|
||||
const result = [];
|
||||
for (const property of properties.values()) {
|
||||
const elementHandle = property.asElement();
|
||||
if (elementHandle)
|
||||
result.push(elementHandle);
|
||||
}
|
||||
return result;
|
||||
$$(selector: string): Promise<ElementHandle[]> {
|
||||
return this._world.$$({ root: this, selector });
|
||||
}
|
||||
|
||||
$eval: types.$Eval = async (selector, pageFunction, ...args) => {
|
||||
const elementHandle = await this.$(selector);
|
||||
if (!elementHandle)
|
||||
throw new Error(`Error: failed to find element matching selector "${selector}"`);
|
||||
const result = await elementHandle.evaluate(pageFunction, ...args as any);
|
||||
await elementHandle.dispose();
|
||||
return result;
|
||||
$eval: types.$Eval = (selector, pageFunction, ...args) => {
|
||||
return this._world.$eval({ root: this, selector }, pageFunction, ...args as any);
|
||||
}
|
||||
|
||||
$$eval: types.$$Eval = async (selector, pageFunction, ...args) => {
|
||||
const arrayHandle = await this.evaluateHandle(
|
||||
(root: SelectorRoot, selector: string, injected: Injected) => injected.querySelectorAll('css=' + selector, root),
|
||||
selector, await this._world.injected()
|
||||
);
|
||||
|
||||
const result = await arrayHandle.evaluate(pageFunction, ...args as any);
|
||||
await arrayHandle.dispose();
|
||||
return result;
|
||||
$$eval: types.$$Eval = (selector, pageFunction, ...args) => {
|
||||
return this._world.$$eval({ root: this, selector }, pageFunction, ...args as any);
|
||||
}
|
||||
|
||||
async $x(expression: string): Promise<ElementHandle[]> {
|
||||
const arrayHandle = await this.evaluateHandle(
|
||||
(root: SelectorRoot, expression: string, injected: Injected) => injected.querySelectorAll('xpath=' + expression, root),
|
||||
expression, await this._world.injected()
|
||||
);
|
||||
const properties = await arrayHandle.getProperties();
|
||||
await arrayHandle.dispose();
|
||||
const result = [];
|
||||
for (const property of properties.values()) {
|
||||
const elementHandle = property.asElement();
|
||||
if (elementHandle)
|
||||
result.push(elementHandle);
|
||||
}
|
||||
return result;
|
||||
$x(expression: string): Promise<ElementHandle[]> {
|
||||
return this._world.$$({ root: this, selector: 'xpath=' + expression });
|
||||
}
|
||||
|
||||
isIntersectingViewport(): Promise<boolean> {
|
||||
|
@ -124,32 +124,27 @@ export class Frame {
|
||||
|
||||
async $(selector: string): Promise<dom.ElementHandle | null> {
|
||||
const domWorld = await this._mainDOMWorld();
|
||||
const document = await domWorld._document();
|
||||
return document.$(selector);
|
||||
return domWorld.$(selector);
|
||||
}
|
||||
|
||||
async $x(expression: string): Promise<dom.ElementHandle[]> {
|
||||
const domWorld = await this._mainDOMWorld();
|
||||
const document = await domWorld._document();
|
||||
return document.$x(expression);
|
||||
return domWorld.$$('xpath=' + expression);
|
||||
}
|
||||
|
||||
$eval: types.$Eval = async (selector, pageFunction, ...args) => {
|
||||
const domWorld = await this._mainDOMWorld();
|
||||
const document = await domWorld._document();
|
||||
return document.$eval(selector, pageFunction, ...args as any);
|
||||
return domWorld.$eval(selector, pageFunction, ...args as any);
|
||||
}
|
||||
|
||||
$$eval: types.$$Eval = async (selector, pageFunction, ...args) => {
|
||||
const domWorld = await this._mainDOMWorld();
|
||||
const document = await domWorld._document();
|
||||
return document.$$eval(selector, pageFunction, ...args as any);
|
||||
return domWorld.$$eval(selector, pageFunction, ...args as any);
|
||||
}
|
||||
|
||||
async $$(selector: string): Promise<dom.ElementHandle[]> {
|
||||
const domWorld = await this._mainDOMWorld();
|
||||
const document = await domWorld._document();
|
||||
return document.$$(selector);
|
||||
return domWorld.$$(selector);
|
||||
}
|
||||
|
||||
async content(): Promise<string> {
|
||||
@ -307,8 +302,7 @@ export class Frame {
|
||||
|
||||
async click(selector: string, options?: ClickOptions) {
|
||||
const domWorld = await this._utilityDOMWorld();
|
||||
const document = await domWorld._document();
|
||||
const handle = await document.$(selector);
|
||||
const handle = await domWorld.$(selector);
|
||||
assert(handle, 'No node found for selector: ' + selector);
|
||||
await handle.click(options);
|
||||
await handle.dispose();
|
||||
@ -316,8 +310,7 @@ export class Frame {
|
||||
|
||||
async dblclick(selector: string, options?: MultiClickOptions) {
|
||||
const domWorld = await this._utilityDOMWorld();
|
||||
const document = await domWorld._document();
|
||||
const handle = await document.$(selector);
|
||||
const handle = await domWorld.$(selector);
|
||||
assert(handle, 'No node found for selector: ' + selector);
|
||||
await handle.dblclick(options);
|
||||
await handle.dispose();
|
||||
@ -325,8 +318,7 @@ export class Frame {
|
||||
|
||||
async tripleclick(selector: string, options?: MultiClickOptions) {
|
||||
const domWorld = await this._utilityDOMWorld();
|
||||
const document = await domWorld._document();
|
||||
const handle = await document.$(selector);
|
||||
const handle = await domWorld.$(selector);
|
||||
assert(handle, 'No node found for selector: ' + selector);
|
||||
await handle.tripleclick(options);
|
||||
await handle.dispose();
|
||||
@ -334,8 +326,7 @@ export class Frame {
|
||||
|
||||
async fill(selector: string, value: string) {
|
||||
const domWorld = await this._utilityDOMWorld();
|
||||
const document = await domWorld._document();
|
||||
const handle = await document.$(selector);
|
||||
const handle = await domWorld.$(selector);
|
||||
assert(handle, 'No node found for selector: ' + selector);
|
||||
await handle.fill(value);
|
||||
await handle.dispose();
|
||||
@ -343,8 +334,7 @@ export class Frame {
|
||||
|
||||
async focus(selector: string) {
|
||||
const domWorld = await this._utilityDOMWorld();
|
||||
const document = await domWorld._document();
|
||||
const handle = await document.$(selector);
|
||||
const handle = await domWorld.$(selector);
|
||||
assert(handle, 'No node found for selector: ' + selector);
|
||||
await handle.focus();
|
||||
await handle.dispose();
|
||||
@ -352,8 +342,7 @@ export class Frame {
|
||||
|
||||
async hover(selector: string, options?: PointerActionOptions) {
|
||||
const domWorld = await this._utilityDOMWorld();
|
||||
const document = await domWorld._document();
|
||||
const handle = await document.$(selector);
|
||||
const handle = await domWorld.$(selector);
|
||||
assert(handle, 'No node found for selector: ' + selector);
|
||||
await handle.hover(options);
|
||||
await handle.dispose();
|
||||
@ -361,23 +350,26 @@ export class Frame {
|
||||
|
||||
async select(selector: string, ...values: (string | dom.ElementHandle | SelectOption)[]): Promise<string[]> {
|
||||
const domWorld = await this._utilityDOMWorld();
|
||||
const document = await domWorld._document();
|
||||
const handle = await document.$(selector);
|
||||
const handle = await domWorld.$(selector);
|
||||
assert(handle, 'No node found for selector: ' + selector);
|
||||
const toDispose: Promise<dom.ElementHandle>[] = [];
|
||||
const adoptedValues = await Promise.all(values.map(async value => {
|
||||
if (value instanceof dom.ElementHandle)
|
||||
return domWorld.adoptElementHandle(value, false /* dispose */);
|
||||
if (value instanceof dom.ElementHandle && value.executionContext() !== domWorld.context) {
|
||||
const adopted = domWorld.adoptElementHandle(value);
|
||||
toDispose.push(adopted);
|
||||
return adopted;
|
||||
}
|
||||
return value;
|
||||
}));
|
||||
const result = await handle.select(...adoptedValues);
|
||||
await handle.dispose();
|
||||
await Promise.all(toDispose.map(handlePromise => handlePromise.then(handle => handle.dispose())));
|
||||
return result;
|
||||
}
|
||||
|
||||
async type(selector: string, text: string, options: { delay: (number | undefined); } | undefined) {
|
||||
const domWorld = await this._utilityDOMWorld();
|
||||
const document = await domWorld._document();
|
||||
const handle = await document.$(selector);
|
||||
const handle = await domWorld.$(selector);
|
||||
assert(handle, 'No node found for selector: ' + selector);
|
||||
await handle.type(text, options);
|
||||
await handle.dispose();
|
||||
@ -410,7 +402,11 @@ export class Frame {
|
||||
return null;
|
||||
}
|
||||
const mainDOMWorld = await this._mainDOMWorld();
|
||||
return mainDOMWorld.adoptElementHandle(handle.asElement(), true /* dispose */);
|
||||
if (handle.executionContext() === mainDOMWorld.context)
|
||||
return handle.asElement();
|
||||
const adopted = await mainDOMWorld.adoptElementHandle(handle.asElement());
|
||||
await handle.dispose();
|
||||
return adopted;
|
||||
}
|
||||
|
||||
async waitForXPath(xpath: string, options: {
|
||||
@ -424,7 +420,11 @@ export class Frame {
|
||||
return null;
|
||||
}
|
||||
const mainDOMWorld = await this._mainDOMWorld();
|
||||
return mainDOMWorld.adoptElementHandle(handle.asElement(), true /* dispose */);
|
||||
if (handle.executionContext() === mainDOMWorld.context)
|
||||
return handle.asElement();
|
||||
const adopted = await mainDOMWorld.adoptElementHandle(handle.asElement());
|
||||
await handle.dispose();
|
||||
return adopted;
|
||||
}
|
||||
|
||||
waitForFunction(
|
||||
|
@ -34,7 +34,7 @@ export class ExecutionContext {
|
||||
}
|
||||
|
||||
_createHandle(remoteObject: any): JSHandle {
|
||||
return (this._domWorld && this._domWorld._createHandle(remoteObject)) || new JSHandle(this, remoteObject);
|
||||
return (this._domWorld && this._domWorld.createHandle(remoteObject)) || new JSHandle(this, remoteObject);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,8 +9,8 @@ type PageFunctionOn<On, Args extends any[], R = any> = string | ((on: On, ...arg
|
||||
|
||||
export type Evaluate = <Args extends any[], R>(pageFunction: PageFunction<Args, R>, ...args: Boxed<Args>) => Promise<R>;
|
||||
export type EvaluateHandle = <Args extends any[]>(pageFunction: PageFunction<Args>, ...args: Boxed<Args>) => Promise<js.JSHandle>;
|
||||
export type $Eval = <Args extends any[], R>(selector: string, pageFunction: PageFunctionOn<Element, Args, R>, ...args: Boxed<Args>) => Promise<R>;
|
||||
export type $$Eval = <Args extends any[], R>(selector: string, pageFunction: PageFunctionOn<Element[], Args, R>, ...args: Boxed<Args>) => Promise<R>;
|
||||
export type $Eval<S = string> = <Args extends any[], R>(selector: S, pageFunction: PageFunctionOn<Element, Args, R>, ...args: Boxed<Args>) => Promise<R>;
|
||||
export type $$Eval<S = string> = <Args extends any[], R>(selector: S, pageFunction: PageFunctionOn<Element[], Args, R>, ...args: Boxed<Args>) => Promise<R>;
|
||||
export type EvaluateOn = <Args extends any[], R>(pageFunction: PageFunctionOn<any, Args, R>, ...args: Boxed<Args>) => Promise<R>;
|
||||
export type EvaluateHandleOn = <Args extends any[]>(pageFunction: PageFunctionOn<any, Args>, ...args: Boxed<Args>) => Promise<js.JSHandle>;
|
||||
|
||||
|
27
test/assets/deep-shadow.html
Normal file
27
test/assets/deep-shadow.html
Normal file
@ -0,0 +1,27 @@
|
||||
<script>
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
const outer = document.createElement('section');
|
||||
document.body.appendChild(outer);
|
||||
|
||||
const root1 = document.createElement('div');
|
||||
outer.appendChild(root1);
|
||||
const shadowRoot1 = root1.attachShadow({mode: 'open'});
|
||||
const span1 = document.createElement('span');
|
||||
span1.textContent = 'Hello from root1';
|
||||
shadowRoot1.appendChild(span1);
|
||||
|
||||
const root2 = document.createElement('div');
|
||||
shadowRoot1.appendChild(root2);
|
||||
const shadowRoot2 = root2.attachShadow({mode: 'open'});
|
||||
const span2 = document.createElement('span');
|
||||
span2.textContent = 'Hello from root2';
|
||||
shadowRoot2.appendChild(span2);
|
||||
|
||||
const root3 = document.createElement('div');
|
||||
shadowRoot1.appendChild(root3);
|
||||
const shadowRoot3 = root3.attachShadow({mode: 'open'});
|
||||
const span3 = document.createElement('span');
|
||||
span3.textContent = 'Hello from root3';
|
||||
shadowRoot3.appendChild(span3);
|
||||
});
|
||||
</script>
|
@ -20,7 +20,17 @@ module.exports.addTests = function({testRunner, expect, product, FFOX, CHROME, W
|
||||
const {beforeAll, beforeEach, afterAll, afterEach} = testRunner;
|
||||
|
||||
describe('Page.$eval', function() {
|
||||
it('should work', async({page, server}) => {
|
||||
it('should work with css selector', async({page, server}) => {
|
||||
await page.setContent('<section id="testAttribute">43543</section>');
|
||||
const idAttribute = await page.$eval('css=section', e => e.id);
|
||||
expect(idAttribute).toBe('testAttribute');
|
||||
});
|
||||
it('should work with xpath selector', async({page, server}) => {
|
||||
await page.setContent('<section id="testAttribute">43543</section>');
|
||||
const idAttribute = await page.$eval('xpath=/html/body/section', e => e.id);
|
||||
expect(idAttribute).toBe('testAttribute');
|
||||
});
|
||||
it('should auto-detect css selector', async({page, server}) => {
|
||||
await page.setContent('<section id="testAttribute">43543</section>');
|
||||
const idAttribute = await page.$eval('section', e => e.id);
|
||||
expect(idAttribute).toBe('testAttribute');
|
||||
@ -41,26 +51,94 @@ module.exports.addTests = function({testRunner, expect, product, FFOX, CHROME, W
|
||||
await page.$eval('section', e => e.id).catch(e => error = e);
|
||||
expect(error.message).toContain('failed to find element matching selector "section"');
|
||||
});
|
||||
it('should support >> syntax', async({page, server}) => {
|
||||
await page.setContent('<section><div>hello</div></section>');
|
||||
const text = await page.$eval('css=section >> css=div', (e, suffix) => e.textContent + suffix, ' world!');
|
||||
expect(text).toBe('hello world!');
|
||||
});
|
||||
it('should support >> syntax with different engines', async({page, server}) => {
|
||||
await page.setContent('<section><div>hello</div></section>');
|
||||
const text = await page.$eval('xpath=/html/body/section >> css=div', (e, suffix) => e.textContent + suffix, ' world!');
|
||||
expect(text).toBe('hello world!');
|
||||
});
|
||||
it('should support spaces with >> syntax', async({page, server}) => {
|
||||
await page.goto(server.PREFIX + '/deep-shadow.html');
|
||||
const text = await page.$eval(' css = div >>css=div>>css = span ', e => e.textContent);
|
||||
expect(text).toBe('Hello from root2');
|
||||
});
|
||||
it('should enter shadow roots with >> syntax', async({page, server}) => {
|
||||
await page.goto(server.PREFIX + '/deep-shadow.html');
|
||||
const text1 = await page.$eval('css=div >> css=span', e => e.textContent);
|
||||
expect(text1).toBe('Hello from root1');
|
||||
const text2 = await page.$eval('css=div >> css=*:nth-child(2) >> css=span', e => e.textContent);
|
||||
expect(text2).toBe('Hello from root2');
|
||||
const nonExisting = await page.$('css=div div >> css=span');
|
||||
expect(nonExisting).not.toBeTruthy();
|
||||
const text3 = await page.$eval('css=section div >> css=span', e => e.textContent);
|
||||
expect(text3).toBe('Hello from root1');
|
||||
const text4 = await page.$eval('xpath=/html/body/section/div >> css=div >> css=span', e => e.textContent);
|
||||
expect(text4).toBe('Hello from root2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Page.$$eval', function() {
|
||||
it('should work', async({page, server}) => {
|
||||
it('should work with css selector', async({page, server}) => {
|
||||
await page.setContent('<div>hello</div><div>beautiful</div><div>world!</div>');
|
||||
const divsCount = await page.$$eval('css=div', divs => divs.length);
|
||||
expect(divsCount).toBe(3);
|
||||
});
|
||||
it('should work with xpath selector', async({page, server}) => {
|
||||
await page.setContent('<div>hello</div><div>beautiful</div><div>world!</div>');
|
||||
const divsCount = await page.$$eval('xpath=/html/body/div', divs => divs.length);
|
||||
expect(divsCount).toBe(3);
|
||||
});
|
||||
it('should auto-detect css selector', async({page, server}) => {
|
||||
await page.setContent('<div>hello</div><div>beautiful</div><div>world!</div>');
|
||||
const divsCount = await page.$$eval('div', divs => divs.length);
|
||||
expect(divsCount).toBe(3);
|
||||
});
|
||||
it('should support >> syntax', async({page, server}) => {
|
||||
await page.setContent('<div><span>hello</span></div><div>beautiful</div><div><span>wo</span><span>rld!</span></div><span>Not this one</span>');
|
||||
const spansCount = await page.$$eval('css=div >> css=span', spans => spans.length);
|
||||
expect(spansCount).toBe(3);
|
||||
});
|
||||
it('should enter shadow roots with >> syntax', async({page, server}) => {
|
||||
await page.goto(server.PREFIX + '/deep-shadow.html');
|
||||
const spansCount = await page.$$eval('css=div >> css=div >> css=span', spans => spans.length);
|
||||
expect(spansCount).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Page.$', function() {
|
||||
it('should query existing element', async({page, server}) => {
|
||||
await page.setContent('<section>test</section>');
|
||||
const element = await page.$('section');
|
||||
const element = await page.$('css=section');
|
||||
expect(element).toBeTruthy();
|
||||
});
|
||||
it('should query existing element with xpath', async({page, server}) => {
|
||||
await page.setContent('<section>test</section>');
|
||||
const element = await page.$('xpath=/html/body/section');
|
||||
expect(element).toBeTruthy();
|
||||
});
|
||||
it('should return null for non-existing element', async({page, server}) => {
|
||||
const element = await page.$('non-existing-element');
|
||||
expect(element).toBe(null);
|
||||
});
|
||||
it('should auto-detect xpath selector', async({page, server}) => {
|
||||
await page.setContent('<section>test</section>');
|
||||
const element = await page.$('//html/body/section');
|
||||
expect(element).toBeTruthy();
|
||||
});
|
||||
it('should auto-detect css selector', async({page, server}) => {
|
||||
await page.setContent('<section>test</section>');
|
||||
const element = await page.$('section');
|
||||
expect(element).toBeTruthy();
|
||||
});
|
||||
it('should support >> syntax', async({page, server}) => {
|
||||
await page.setContent('<section><div>test</div></section>');
|
||||
const element = await page.$('css=section >> css=div');
|
||||
expect(element).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Page.$$', function() {
|
||||
@ -136,7 +214,7 @@ module.exports.addTests = function({testRunner, expect, product, FFOX, CHROME, W
|
||||
await page.setContent(htmlContent);
|
||||
const elementHandle = await page.$('#myId');
|
||||
const errorMessage = await elementHandle.$eval('.a', node => node.innerText).catch(error => error.message);
|
||||
expect(errorMessage).toBe(`Error: failed to find element matching selector ".a"`);
|
||||
expect(errorMessage).toBe(`Error: failed to find element matching selector ":scope >> .a"`);
|
||||
});
|
||||
});
|
||||
describe('ElementHandle.$$eval', function() {
|
||||
|
Loading…
Reference in New Issue
Block a user