mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat(api): introduce selectors.register method (#701)
This commit is contained in:
parent
9cd61571f1
commit
2bef4aea03
63
docs/api.md
63
docs/api.md
@ -18,6 +18,7 @@
|
||||
- [class: Mouse](#class-mouse)
|
||||
- [class: Request](#class-request)
|
||||
- [class: Response](#class-response)
|
||||
- [class: Selectors](#class-selectors)
|
||||
- [class: WebSocket](#class-websocket)
|
||||
- [class: TimeoutError](#class-timeouterror)
|
||||
- [class: Accessibility](#class-accessibility)
|
||||
@ -57,6 +58,7 @@ Playwright automatically downloads browser executables during installation, see
|
||||
- [playwright.devices](#playwrightdevices)
|
||||
- [playwright.errors](#playwrighterrors)
|
||||
- [playwright.firefox](#playwrightfirefox)
|
||||
- [playwright.selectors](#playwrightselectors)
|
||||
- [playwright.webkit](#playwrightwebkit)
|
||||
<!-- GEN:stop -->
|
||||
|
||||
@ -113,6 +115,11 @@ try {
|
||||
|
||||
This object can be used to launch or connect to Firefox, returning instances of [FirefoxBrowser].
|
||||
|
||||
#### playwright.selectors
|
||||
- returns: <[Selectors]>
|
||||
|
||||
Selectors can be used to install custom selector engines. See [Working with selectors](#working-with-selectors) for more information.
|
||||
|
||||
#### playwright.webkit
|
||||
- returns: <[BrowserType]>
|
||||
|
||||
@ -3016,6 +3023,61 @@ Contains the status text of the response (e.g. usually an "OK" for a success).
|
||||
|
||||
Contains the URL of the response.
|
||||
|
||||
### class: Selectors
|
||||
|
||||
Selectors can be used to install custom selector engines. See [Working with selectors](#working-with-selectors) for more information.
|
||||
|
||||
<!-- GEN:toc -->
|
||||
- [selectors.register(engineSource)](#selectorsregisterenginesource)
|
||||
<!-- GEN:stop -->
|
||||
|
||||
#### selectors.register(engineSource)
|
||||
- `engineSource` <[string]> String which evaluates to a selector engine instance.
|
||||
- returns: <[Promise]>
|
||||
|
||||
An example of registering selector engine which selects nodes based on tag name:
|
||||
```js
|
||||
const { selectors, firefox } = require('playwright'); // Or 'chromium' or 'webkit'.
|
||||
|
||||
(async () => {
|
||||
const createTagSelector = () => ({
|
||||
// Selectors will be prefixed with "tag=".
|
||||
name: 'tag',
|
||||
|
||||
// Creates a selector which matches given target when queried at the root.
|
||||
create(root, target) {
|
||||
return target.tagName;
|
||||
},
|
||||
|
||||
// Returns the first element matching given selector in the root's subtree.
|
||||
query(root, selector) {
|
||||
return root.querySelector(selector);
|
||||
},
|
||||
|
||||
// Returns all elements matching given selector in the root's subtree.
|
||||
queryAll(root, selector) {
|
||||
return Array.from(root.querySelectorAll(selector));
|
||||
}
|
||||
});
|
||||
|
||||
// Construct an expression which evaluates to our selector instance.
|
||||
await selectors.register(`(${createTagSelector.toString()})()`);
|
||||
|
||||
const browser = await firefox.launch();
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage('http://example.com');
|
||||
|
||||
// Use the selector prefixed with its name.
|
||||
const button = await page.$('tag=button');
|
||||
// Combine it with other selector engines.
|
||||
await page.click('tag=div >> text="Click me"');
|
||||
// Can use it in any methods supporting selectors.
|
||||
const buttonCount = await page.$$eval('tag=button', buttons => buttons.length);
|
||||
|
||||
await browser.close();
|
||||
})();
|
||||
```
|
||||
|
||||
### class: WebSocket
|
||||
|
||||
The [WebSocket] class represents websocket connections in the page.
|
||||
@ -3779,6 +3841,7 @@ const { chromium } = require('playwright');
|
||||
[RegExp]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp
|
||||
[Request]: #class-request "Request"
|
||||
[Response]: #class-response "Response"
|
||||
[Selectors]: #class-selectors "Selectors"
|
||||
[Serializable]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#Description "Serializable"
|
||||
[Target]: #class-target "Target"
|
||||
[TimeoutError]: #class-timeouterror "TimeoutError"
|
||||
|
||||
1
index.d.ts
vendored
1
index.d.ts
vendored
@ -20,4 +20,5 @@ export const errors: { TimeoutError: typeof import('./lib/errors').TimeoutError
|
||||
export const chromium: import('./lib/server/chromium').Chromium;
|
||||
export const firefox: import('./lib/server/firefox').Firefox;
|
||||
export const webkit: import('./lib/server/webkit').WebKit;
|
||||
export const selectors: import('./lib/api').Selectors;
|
||||
export type PlaywrightWeb = typeof import('./lib/web');
|
||||
|
||||
1
index.js
1
index.js
@ -31,6 +31,7 @@ for (const className in api) {
|
||||
module.exports = {
|
||||
devices: DeviceDescriptors,
|
||||
errors: { TimeoutError },
|
||||
selectors: api.Selectors._instance(),
|
||||
chromium: new Chromium(__dirname, packageJson.playwright.chromium_revision),
|
||||
firefox: new Firefox(__dirname, packageJson.playwright.firefox_revision),
|
||||
webkit: new WebKit(__dirname, packageJson.playwright.webkit_revision),
|
||||
|
||||
@ -26,6 +26,7 @@ export { Keyboard, Mouse } from './input';
|
||||
export { JSHandle } from './javascript';
|
||||
export { Request, Response, WebSocket } from './network';
|
||||
export { Coverage, FileChooser, Page, Worker } from './page';
|
||||
export { Selectors } from './selectors';
|
||||
|
||||
export { CRBrowser as ChromiumBrowser } from './chromium/crBrowser';
|
||||
export { CRSession as ChromiumSession } from './chromium/crConnection';
|
||||
|
||||
12
src/dom.ts
12
src/dom.ts
@ -19,16 +19,17 @@ import * as input from './input';
|
||||
import * as js from './javascript';
|
||||
import * as types from './types';
|
||||
import * as injectedSource from './generated/injectedSource';
|
||||
import * as zsSelectorEngineSource from './generated/zsSelectorEngineSource';
|
||||
import { assert, helper, debugError } from './helper';
|
||||
import Injected from './injected/injected';
|
||||
import { Page } from './page';
|
||||
import * as platform from './platform';
|
||||
import { Selectors } from './selectors';
|
||||
|
||||
export class FrameExecutionContext extends js.ExecutionContext {
|
||||
readonly frame: frames.Frame;
|
||||
|
||||
private _injectedPromise?: Promise<js.JSHandle>;
|
||||
private _injectedGeneration = -1;
|
||||
|
||||
constructor(delegate: js.ExecutionContextDelegate, frame: frames.Frame) {
|
||||
super(delegate);
|
||||
@ -69,14 +70,19 @@ export class FrameExecutionContext extends js.ExecutionContext {
|
||||
}
|
||||
|
||||
_injected(): Promise<js.JSHandle> {
|
||||
const selectors = Selectors._instance();
|
||||
if (this._injectedPromise && selectors._generation !== this._injectedGeneration) {
|
||||
this._injectedPromise.then(handle => handle.dispose());
|
||||
this._injectedPromise = undefined;
|
||||
}
|
||||
if (!this._injectedPromise) {
|
||||
const additionalEngineSources = [zsSelectorEngineSource.source];
|
||||
const source = `
|
||||
new (${injectedSource.source})([
|
||||
${additionalEngineSources.join(',\n')},
|
||||
${selectors._sources.join(',\n')},
|
||||
])
|
||||
`;
|
||||
this._injectedPromise = this.evaluateHandle(source);
|
||||
this._injectedGeneration = selectors._generation;
|
||||
}
|
||||
return this._injectedPromise;
|
||||
}
|
||||
|
||||
@ -27,7 +27,6 @@ import * as types from './types';
|
||||
import { Events } from './events';
|
||||
import { BrowserContext } from './browserContext';
|
||||
import { ConsoleMessage, ConsoleMessageLocation } from './console';
|
||||
import Injected from './injected/injected';
|
||||
import * as accessibility from './accessibility';
|
||||
import * as platform from './platform';
|
||||
|
||||
@ -197,13 +196,6 @@ export class Page extends platform.EventEmitter {
|
||||
return this.mainFrame().waitForSelector(selector, options);
|
||||
}
|
||||
|
||||
async _createSelector(name: string, handle: dom.ElementHandle<Element>): Promise<string | undefined> {
|
||||
const mainContext = await this.mainFrame()._mainContext();
|
||||
return mainContext.evaluate((injected: Injected, target: Element, name: string) => {
|
||||
return injected.engines.get(name)!.create(document.documentElement, target);
|
||||
}, await mainContext._injected(), handle, name);
|
||||
}
|
||||
|
||||
evaluateHandle: types.EvaluateHandle = async (pageFunction, ...args) => {
|
||||
return this.mainFrame().evaluateHandle(pageFunction, ...args as any);
|
||||
}
|
||||
|
||||
48
src/selectors.ts
Normal file
48
src/selectors.ts
Normal file
@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as zsSelectorEngineSource from './generated/zsSelectorEngineSource';
|
||||
import * as dom from './dom';
|
||||
import Injected from './injected/injected';
|
||||
|
||||
let selectors: Selectors;
|
||||
|
||||
export class Selectors {
|
||||
readonly _sources: string[];
|
||||
_generation = 0;
|
||||
|
||||
static _instance() {
|
||||
if (!selectors)
|
||||
selectors = new Selectors();
|
||||
return selectors;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this._sources = [zsSelectorEngineSource.source];
|
||||
}
|
||||
|
||||
async register(engineSource: string) {
|
||||
this._sources.push(engineSource);
|
||||
++this._generation;
|
||||
}
|
||||
|
||||
async _createSelector(name: string, handle: dom.ElementHandle<Element>): Promise<string | undefined> {
|
||||
const mainContext = await handle._page.mainFrame()._mainContext();
|
||||
return mainContext.evaluate((injected: Injected, target: Element, name: string) => {
|
||||
return injected.engines.get(name)!.create(document.documentElement, target);
|
||||
}, await mainContext._injected(), handle, name);
|
||||
}
|
||||
}
|
||||
@ -36,7 +36,8 @@ module.exports.describe = ({testRunner, product, playwrightPath}) => {
|
||||
const LINUX = os.platform() === 'linux';
|
||||
const WIN = os.platform() === 'win32';
|
||||
|
||||
const playwright = require(playwrightPath)[product.toLowerCase()];
|
||||
const playwrightModule = require(playwrightPath);
|
||||
const playwright = playwrightModule[product.toLowerCase()];
|
||||
|
||||
const headless = (process.env.HEADLESS || 'true').trim().toLowerCase() === 'true';
|
||||
const slowMo = parseInt((process.env.SLOW_MO || '0').trim(), 10);
|
||||
@ -81,6 +82,7 @@ module.exports.describe = ({testRunner, product, playwrightPath}) => {
|
||||
LINUX,
|
||||
WIN,
|
||||
playwright,
|
||||
selectors: playwrightModule.selectors,
|
||||
expect,
|
||||
defaultBrowserOptions,
|
||||
playwrightPath,
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
module.exports.describe = function({testRunner, expect, product, FFOX, CHROMIUM, WEBKIT}) {
|
||||
module.exports.describe = function({testRunner, expect, selectors, FFOX, CHROMIUM, WEBKIT}) {
|
||||
const {describe, xdescribe, fdescribe} = testRunner;
|
||||
const {it, fit, xit, dit} = testRunner;
|
||||
const {beforeAll, beforeEach, afterAll, afterEach} = testRunner;
|
||||
@ -393,28 +393,28 @@ module.exports.describe = function({testRunner, expect, product, FFOX, CHROMIUM,
|
||||
|
||||
it('create', async ({page}) => {
|
||||
await page.setContent(`<div>yo</div><div>ya</div><div>ya</div>`);
|
||||
expect(await page._createSelector('zs', await page.$('div'))).toBe('"yo"');
|
||||
expect(await page._createSelector('zs', await page.$('div:nth-child(2)'))).toBe('"ya"');
|
||||
expect(await page._createSelector('zs', await page.$('div:nth-child(3)'))).toBe('"ya"#1');
|
||||
expect(await selectors._createSelector('zs', await page.$('div'))).toBe('"yo"');
|
||||
expect(await selectors._createSelector('zs', await page.$('div:nth-child(2)'))).toBe('"ya"');
|
||||
expect(await selectors._createSelector('zs', await page.$('div:nth-child(3)'))).toBe('"ya"#1');
|
||||
|
||||
await page.setContent(`<img alt="foo bar">`);
|
||||
expect(await page._createSelector('zs', await page.$('img'))).toBe('img[alt="foo bar"]');
|
||||
expect(await selectors._createSelector('zs', await page.$('img'))).toBe('img[alt="foo bar"]');
|
||||
|
||||
await page.setContent(`<div>yo<span></span></div><span></span>`);
|
||||
expect(await page._createSelector('zs', await page.$('span'))).toBe('"yo"~SPAN');
|
||||
expect(await page._createSelector('zs', await page.$('span:nth-child(2)'))).toBe('SPAN#1');
|
||||
expect(await selectors._createSelector('zs', await page.$('span'))).toBe('"yo"~SPAN');
|
||||
expect(await selectors._createSelector('zs', await page.$('span:nth-child(2)'))).toBe('SPAN#1');
|
||||
});
|
||||
|
||||
it('children of various display parents', async ({page}) => {
|
||||
await page.setContent(`<body><div style='position: fixed;'><span>yo</span></div></body>`);
|
||||
expect(await page._createSelector('zs', await page.$('span'))).toBe('"yo"');
|
||||
expect(await selectors._createSelector('zs', await page.$('span'))).toBe('"yo"');
|
||||
|
||||
await page.setContent(`<div style='position: relative;'><span>yo</span></div>`);
|
||||
expect(await page._createSelector('zs', await page.$('span'))).toBe('"yo"');
|
||||
expect(await selectors._createSelector('zs', await page.$('span'))).toBe('"yo"');
|
||||
|
||||
// "display: none" makes all children text invisible - fallback to tag name.
|
||||
await page.setContent(`<div style='display: none;'><span>yo</span></div>`);
|
||||
expect(await page._createSelector('zs', await page.$('span'))).toBe('SPAN');
|
||||
expect(await selectors._createSelector('zs', await page.$('span'))).toBe('SPAN');
|
||||
});
|
||||
|
||||
it('boundary', async ({page}) => {
|
||||
@ -465,7 +465,7 @@ module.exports.describe = function({testRunner, expect, product, FFOX, CHROMIUM,
|
||||
<div id=target2>hello</div>
|
||||
</div>
|
||||
</div>`);
|
||||
expect(await page._createSelector('zs', await page.$('#target'))).toBe('"ya"~"hey"~"hello"');
|
||||
expect(await selectors._createSelector('zs', await page.$('#target'))).toBe('"ya"~"hey"~"hello"');
|
||||
expect(await page.$eval(`zs="ya"~"hey"~"hello"`, e => e.outerHTML)).toBe('<div id="target">hello</div>');
|
||||
expect(await page.$eval(`zs="ya"~"hey"~"unique"`, e => e.outerHTML).catch(e => e.message)).toBe('Error: failed to find element matching selector "zs="ya"~"hey"~"unique""');
|
||||
expect(await page.$$eval(`zs="ya" ~ "hey" ~ "hello"`, es => es.map(e => e.outerHTML).join('\n'))).toBe('<div id="target">hello</div>\n<div id="target2">hello</div>');
|
||||
@ -498,18 +498,63 @@ module.exports.describe = function({testRunner, expect, product, FFOX, CHROMIUM,
|
||||
|
||||
it('create', async ({page}) => {
|
||||
await page.setContent(`<div>yo</div><div>"ya</div><div>ye ye</div>`);
|
||||
expect(await page._createSelector('text', await page.$('div'))).toBe('yo');
|
||||
expect(await page._createSelector('text', await page.$('div:nth-child(2)'))).toBe('"\\"ya"');
|
||||
expect(await page._createSelector('text', await page.$('div:nth-child(3)'))).toBe('"ye ye"');
|
||||
expect(await selectors._createSelector('text', await page.$('div'))).toBe('yo');
|
||||
expect(await selectors._createSelector('text', await page.$('div:nth-child(2)'))).toBe('"\\"ya"');
|
||||
expect(await selectors._createSelector('text', await page.$('div:nth-child(3)'))).toBe('"ye ye"');
|
||||
|
||||
await page.setContent(`<div>yo</div><div>yo<div>ya</div>hey</div>`);
|
||||
expect(await page._createSelector('text', await page.$('div:nth-child(2)'))).toBe('hey');
|
||||
expect(await selectors._createSelector('text', await page.$('div:nth-child(2)'))).toBe('hey');
|
||||
|
||||
await page.setContent(`<div> yo <div></div>ya</div>`);
|
||||
expect(await page._createSelector('text', await page.$('div'))).toBe('yo');
|
||||
expect(await selectors._createSelector('text', await page.$('div'))).toBe('yo');
|
||||
|
||||
await page.setContent(`<div> "yo <div></div>ya</div>`);
|
||||
expect(await page._createSelector('text', await page.$('div'))).toBe('" \\"yo "');
|
||||
expect(await selectors._createSelector('text', await page.$('div'))).toBe('" \\"yo "');
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectors.register', () => {
|
||||
it('should work', async ({page}) => {
|
||||
const createTagSelector = () => ({
|
||||
name: 'tag',
|
||||
create(root, target) {
|
||||
return target.nodeName;
|
||||
},
|
||||
query(root, selector) {
|
||||
return root.querySelector(selector);
|
||||
},
|
||||
queryAll(root, selector) {
|
||||
return Array.from(root.querySelectorAll(selector));
|
||||
}
|
||||
});
|
||||
await selectors.register(`(${createTagSelector.toString()})()`);
|
||||
await page.setContent('<div><span></span></div><div></div>');
|
||||
expect(await selectors._createSelector('tag', await page.$('div'))).toBe('DIV');
|
||||
expect(await page.$eval('tag=DIV', e => e.nodeName)).toBe('DIV');
|
||||
expect(await page.$eval('tag=SPAN', e => e.nodeName)).toBe('SPAN');
|
||||
expect(await page.$$eval('tag=DIV', es => es.length)).toBe(2);
|
||||
});
|
||||
it('should update', async ({page}) => {
|
||||
await page.setContent('<div><dummy id=d1></dummy></div><span><dummy id=d2></dummy></span>');
|
||||
expect(await page.$eval('div', e => e.nodeName)).toBe('DIV');
|
||||
const error = await page.$('dummy=foo').catch(e => e);
|
||||
expect(error.message).toContain('Unknown engine dummy while parsing selector dummy=foo');
|
||||
await selectors.register(`
|
||||
({
|
||||
name: 'dummy',
|
||||
create(root, target) {
|
||||
return target.nodeName;
|
||||
},
|
||||
query(root, selector) {
|
||||
return root.querySelector('dummy');
|
||||
},
|
||||
queryAll(root, selector) {
|
||||
return Array.from(root.querySelectorAll('dummy'));
|
||||
}
|
||||
})
|
||||
`);
|
||||
expect(await page.$eval('dummy=foo', e => e.id)).toBe('d1');
|
||||
expect(await page.$eval('css=span >> dummy=foo', e => e.id)).toBe('d2');
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user