mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat(rpc): support selectors (#2936)
This commit is contained in:
parent
6c75cbe5f5
commit
9fdb3e23c3
@ -30,9 +30,17 @@ export type PlaywrightInitializer = {
|
||||
firefox: BrowserTypeChannel,
|
||||
webkit: BrowserTypeChannel,
|
||||
deviceDescriptors: types.Devices,
|
||||
selectors: SelectorsChannel,
|
||||
};
|
||||
|
||||
|
||||
export interface SelectorsChannel extends Channel {
|
||||
register(params: { name: string, source: string, options: { contentScript?: boolean } }): Promise<void>;
|
||||
createSelector(params: { name: string, handle: ElementHandleChannel }): Promise<string | undefined>;
|
||||
}
|
||||
export type SelectorsInitializer = {};
|
||||
|
||||
|
||||
export interface BrowserTypeChannel extends Channel {
|
||||
connect(params: types.ConnectOptions): Promise<BrowserChannel>;
|
||||
launch(params: types.LaunchOptions): Promise<BrowserChannel>;
|
||||
|
||||
@ -35,6 +35,7 @@ import { Playwright } from './playwright';
|
||||
import { Channel } from '../channels';
|
||||
import { ChromiumBrowser } from './chromiumBrowser';
|
||||
import { ChromiumBrowserContext } from './chromiumBrowserContext';
|
||||
import { Selectors } from './selectors';
|
||||
|
||||
class Root extends ChannelOwner<Channel, {}> {
|
||||
constructor(connection: Connection) {
|
||||
@ -190,6 +191,9 @@ export class Connection {
|
||||
case 'route':
|
||||
result = new Route(parent, type, guid, initializer);
|
||||
break;
|
||||
case 'selectors':
|
||||
result = new Selectors(parent, type, guid, initializer);
|
||||
break;
|
||||
case 'worker':
|
||||
result = new Worker(parent, type, guid, initializer);
|
||||
break;
|
||||
|
||||
@ -18,12 +18,14 @@ import { PlaywrightChannel, PlaywrightInitializer } from '../channels';
|
||||
import * as types from '../../types';
|
||||
import { BrowserType } from './browserType';
|
||||
import { ChannelOwner } from './channelOwner';
|
||||
import { Selectors } from './selectors';
|
||||
|
||||
export class Playwright extends ChannelOwner<PlaywrightChannel, PlaywrightInitializer> {
|
||||
chromium: BrowserType;
|
||||
firefox: BrowserType;
|
||||
webkit: BrowserType;
|
||||
devices: types.Devices;
|
||||
selectors: Selectors;
|
||||
|
||||
constructor(parent: ChannelOwner, type: string, guid: string, initializer: PlaywrightInitializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
@ -31,5 +33,6 @@ export class Playwright extends ChannelOwner<PlaywrightChannel, PlaywrightInitia
|
||||
this.firefox = BrowserType.from(initializer.firefox);
|
||||
this.webkit = BrowserType.from(initializer.webkit);
|
||||
this.devices = initializer.deviceDescriptors;
|
||||
this.selectors = Selectors.from(initializer.selectors);
|
||||
}
|
||||
}
|
||||
|
||||
39
src/rpc/client/selectors.ts
Normal file
39
src/rpc/client/selectors.ts
Normal file
@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 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 { SelectorsChannel, SelectorsInitializer } from '../channels';
|
||||
import { ChannelOwner } from './channelOwner';
|
||||
import { helper } from '../../helper';
|
||||
import { ElementHandle } from './elementHandle';
|
||||
|
||||
export class Selectors extends ChannelOwner<SelectorsChannel, SelectorsInitializer> {
|
||||
static from(selectors: SelectorsChannel): Selectors {
|
||||
return (selectors as any)._object;
|
||||
}
|
||||
|
||||
constructor(parent: ChannelOwner, type: string, guid: string, initializer: SelectorsInitializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
}
|
||||
|
||||
async register(name: string, script: string | Function | { path?: string, content?: string }, options: { contentScript?: boolean } = {}): Promise<void> {
|
||||
const source = await helper.evaluationScript(script, undefined, false);
|
||||
await this._channel.register({ name, source, options });
|
||||
}
|
||||
|
||||
async _createSelector(name: string, handle: ElementHandle<Element>): Promise<string | undefined> {
|
||||
return this._channel.createSelector({ name, handle: handle._elementChannel });
|
||||
}
|
||||
}
|
||||
@ -18,6 +18,7 @@ import { Playwright } from '../../server/playwright';
|
||||
import { PlaywrightChannel, PlaywrightInitializer } from '../channels';
|
||||
import { BrowserTypeDispatcher } from './browserTypeDispatcher';
|
||||
import { Dispatcher, DispatcherScope } from './dispatcher';
|
||||
import { SelectorsDispatcher } from './selectorsDispatcher';
|
||||
|
||||
export class PlaywrightDispatcher extends Dispatcher<Playwright, PlaywrightInitializer> implements PlaywrightChannel {
|
||||
constructor(scope: DispatcherScope, playwright: Playwright) {
|
||||
@ -25,7 +26,8 @@ export class PlaywrightDispatcher extends Dispatcher<Playwright, PlaywrightIniti
|
||||
chromium: new BrowserTypeDispatcher(scope, playwright.chromium!),
|
||||
firefox: new BrowserTypeDispatcher(scope, playwright.firefox!),
|
||||
webkit: new BrowserTypeDispatcher(scope, playwright.webkit!),
|
||||
deviceDescriptors: playwright.devices
|
||||
deviceDescriptors: playwright.devices,
|
||||
selectors: new SelectorsDispatcher(scope, playwright.selectors),
|
||||
}, false, 'playwright');
|
||||
}
|
||||
}
|
||||
|
||||
35
src/rpc/server/selectorsDispatcher.ts
Normal file
35
src/rpc/server/selectorsDispatcher.ts
Normal file
@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 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 { Dispatcher, DispatcherScope } from './dispatcher';
|
||||
import { SelectorsInitializer, SelectorsChannel } from '../channels';
|
||||
import { Selectors } from '../../selectors';
|
||||
import { ElementHandleDispatcher } from './elementHandlerDispatcher';
|
||||
import * as dom from '../../dom';
|
||||
|
||||
export class SelectorsDispatcher extends Dispatcher<Selectors, SelectorsInitializer> implements SelectorsChannel {
|
||||
constructor(scope: DispatcherScope, selectors: Selectors) {
|
||||
super(scope, selectors, 'selectors', {});
|
||||
}
|
||||
|
||||
async register(params: { name: string, source: string, options: { contentScript?: boolean } }): Promise<void> {
|
||||
await this._object.register(params.name, params.source, params.options);
|
||||
}
|
||||
|
||||
async createSelector(params: { name: string, handle: ElementHandleDispatcher }): Promise<string | undefined> {
|
||||
return this._object._createSelector(params.name, params.handle._object as dom.ElementHandle<Element>);
|
||||
}
|
||||
}
|
||||
@ -32,6 +32,7 @@ describe.skip(!CHANNEL)('Channels', function() {
|
||||
{ _guid: 'browserType', objects: [] },
|
||||
{ _guid: 'browserType', objects: [] },
|
||||
{ _guid: 'playwright' },
|
||||
{ _guid: 'selectors' },
|
||||
]
|
||||
};
|
||||
await expectScopeState(browser, GOLDEN_PRECONDITION);
|
||||
@ -55,6 +56,7 @@ describe.skip(!CHANNEL)('Channels', function() {
|
||||
] },
|
||||
] },
|
||||
{ _guid: 'playwright' },
|
||||
{ _guid: 'selectors' },
|
||||
]
|
||||
});
|
||||
|
||||
@ -72,6 +74,7 @@ describe.skip(!CHANNEL)('Channels', function() {
|
||||
{ _guid: 'browserType', objects: [] },
|
||||
{ _guid: 'browserType', objects: [] },
|
||||
{ _guid: 'playwright' },
|
||||
{ _guid: 'selectors' },
|
||||
]
|
||||
};
|
||||
await expectScopeState(browserType, GOLDEN_PRECONDITION);
|
||||
@ -88,6 +91,7 @@ describe.skip(!CHANNEL)('Channels', function() {
|
||||
{ _guid: 'browserType', objects: [] },
|
||||
{ _guid: 'browserType', objects: [] },
|
||||
{ _guid: 'playwright' },
|
||||
{ _guid: 'selectors' },
|
||||
]
|
||||
});
|
||||
|
||||
@ -105,6 +109,7 @@ describe.skip(!CHANNEL)('Channels', function() {
|
||||
{ _guid: 'browserType', objects: [] },
|
||||
{ _guid: 'browserType', objects: [] },
|
||||
{ _guid: 'playwright' },
|
||||
{ _guid: 'selectors' },
|
||||
]
|
||||
};
|
||||
await expectScopeState(browserType, GOLDEN_PRECONDITION);
|
||||
@ -123,6 +128,7 @@ describe.skip(!CHANNEL)('Channels', function() {
|
||||
{ _guid: 'browserType', objects: [] },
|
||||
{ _guid: 'browserType', objects: [] },
|
||||
{ _guid: 'playwright' },
|
||||
{ _guid: 'selectors' },
|
||||
]
|
||||
});
|
||||
|
||||
|
||||
@ -97,7 +97,7 @@ describe('Page.dispatchEvent(click)', function() {
|
||||
await watchdog;
|
||||
expect(await page.evaluate(() => window.clicked)).toBe(true);
|
||||
});
|
||||
it.skip(USES_HOOKS)('should be atomic', async({page}) => {
|
||||
it('should be atomic', async({playwright, page}) => {
|
||||
const createDummySelector = () => ({
|
||||
create(root, target) {},
|
||||
query(root, selector) {
|
||||
@ -113,7 +113,7 @@ describe('Page.dispatchEvent(click)', function() {
|
||||
return result;
|
||||
}
|
||||
});
|
||||
await utils.registerEngine('dispatchEvent', createDummySelector);
|
||||
await utils.registerEngine(playwright, 'dispatchEvent', createDummySelector);
|
||||
await page.setContent(`<div onclick="window._clicked=true">Hello</div>`);
|
||||
await page.dispatchEvent('dispatchEvent=div', 'click');
|
||||
expect(await page.evaluate(() => window._clicked)).toBe(true);
|
||||
|
||||
@ -484,7 +484,7 @@ describe('ElementHandle convenience API', function() {
|
||||
expect(await handle.textContent()).toBe('Text,\nmore text');
|
||||
expect(await page.textContent('#inner')).toBe('Text,\nmore text');
|
||||
});
|
||||
it.skip(USES_HOOKS)('textContent should be atomic', async({page}) => {
|
||||
it('textContent should be atomic', async({playwright, page}) => {
|
||||
const createDummySelector = () => ({
|
||||
create(root, target) {},
|
||||
query(root, selector) {
|
||||
@ -500,13 +500,13 @@ describe('ElementHandle convenience API', function() {
|
||||
return result;
|
||||
}
|
||||
});
|
||||
await utils.registerEngine('textContent', createDummySelector);
|
||||
await utils.registerEngine(playwright, 'textContent', createDummySelector);
|
||||
await page.setContent(`<div>Hello</div>`);
|
||||
const tc = await page.textContent('textContent=div');
|
||||
expect(tc).toBe('Hello');
|
||||
expect(await page.evaluate(() => document.querySelector('div').textContent)).toBe('modified');
|
||||
});
|
||||
it.skip(USES_HOOKS)('innerText should be atomic', async({page}) => {
|
||||
it('innerText should be atomic', async({playwright, page}) => {
|
||||
const createDummySelector = () => ({
|
||||
create(root, target) {},
|
||||
query(root, selector) {
|
||||
@ -522,13 +522,13 @@ describe('ElementHandle convenience API', function() {
|
||||
return result;
|
||||
}
|
||||
});
|
||||
await utils.registerEngine('innerText', createDummySelector);
|
||||
await utils.registerEngine(playwright, 'innerText', createDummySelector);
|
||||
await page.setContent(`<div>Hello</div>`);
|
||||
const tc = await page.innerText('innerText=div');
|
||||
expect(tc).toBe('Hello');
|
||||
expect(await page.evaluate(() => document.querySelector('div').innerText)).toBe('modified');
|
||||
});
|
||||
it.skip(USES_HOOKS)('innerHTML should be atomic', async({page}) => {
|
||||
it('innerHTML should be atomic', async({playwright, page}) => {
|
||||
const createDummySelector = () => ({
|
||||
create(root, target) {},
|
||||
query(root, selector) {
|
||||
@ -544,13 +544,13 @@ describe('ElementHandle convenience API', function() {
|
||||
return result;
|
||||
}
|
||||
});
|
||||
await utils.registerEngine('innerHTML', createDummySelector);
|
||||
await utils.registerEngine(playwright, 'innerHTML', createDummySelector);
|
||||
await page.setContent(`<div>Hello<span>world</span></div>`);
|
||||
const tc = await page.innerHTML('innerHTML=div');
|
||||
expect(tc).toBe('Hello<span>world</span>');
|
||||
expect(await page.evaluate(() => document.querySelector('div').innerHTML)).toBe('modified');
|
||||
});
|
||||
it.skip(USES_HOOKS)('getAttribute should be atomic', async({page}) => {
|
||||
it('getAttribute should be atomic', async({playwright, page}) => {
|
||||
const createDummySelector = () => ({
|
||||
create(root, target) {},
|
||||
query(root, selector) {
|
||||
@ -566,7 +566,7 @@ describe('ElementHandle convenience API', function() {
|
||||
return result;
|
||||
}
|
||||
});
|
||||
await utils.registerEngine('getAttribute', createDummySelector);
|
||||
await utils.registerEngine(playwright, 'getAttribute', createDummySelector);
|
||||
await page.setContent(`<div foo=hello></div>`);
|
||||
const tc = await page.getAttribute('getAttribute=div', 'foo');
|
||||
expect(tc).toBe('hello');
|
||||
|
||||
@ -743,8 +743,8 @@ describe('attribute selector', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe.skip(USES_HOOKS)('selectors.register', () => {
|
||||
it.skip(CHANNEL)('should work', async ({page}) => {
|
||||
describe('selectors.register', () => {
|
||||
it('should work', async ({playwright, page}) => {
|
||||
const createTagSelector = () => ({
|
||||
create(root, target) {
|
||||
return target.nodeName;
|
||||
@ -756,7 +756,7 @@ describe.skip(USES_HOOKS)('selectors.register', () => {
|
||||
return Array.from(root.querySelectorAll(selector));
|
||||
}
|
||||
});
|
||||
await utils.registerEngine('tag', `(${createTagSelector.toString()})()`);
|
||||
await utils.registerEngine(playwright, 'tag', `(${createTagSelector.toString()})()`);
|
||||
await page.setContent('<div><span></span></div><div></div>');
|
||||
expect(await playwright.selectors._createSelector('tag', await page.$('div'))).toBe('DIV');
|
||||
expect(await page.$eval('tag=DIV', e => e.nodeName)).toBe('DIV');
|
||||
@ -767,12 +767,12 @@ describe.skip(USES_HOOKS)('selectors.register', () => {
|
||||
const error = await page.$('tAG=DIV').catch(e => e);
|
||||
expect(error.message).toBe('Unknown engine "tAG" while parsing selector tAG=DIV');
|
||||
});
|
||||
it('should work with path', async ({page}) => {
|
||||
await utils.registerEngine('foo', { path: path.join(__dirname, 'assets/sectionselectorengine.js') });
|
||||
it('should work with path', async ({playwright, page}) => {
|
||||
await utils.registerEngine(playwright, 'foo', { path: path.join(__dirname, 'assets/sectionselectorengine.js') });
|
||||
await page.setContent('<section></section>');
|
||||
expect(await page.$eval('foo=whatever', e => e.nodeName)).toBe('SECTION');
|
||||
});
|
||||
it('should work in main and isolated world', async ({page}) => {
|
||||
it('should work in main and isolated world', async ({playwright, page}) => {
|
||||
const createDummySelector = () => ({
|
||||
create(root, target) { },
|
||||
query(root, selector) {
|
||||
@ -782,8 +782,8 @@ describe.skip(USES_HOOKS)('selectors.register', () => {
|
||||
return [document.body, document.documentElement, window.__answer];
|
||||
}
|
||||
});
|
||||
await utils.registerEngine('main', createDummySelector);
|
||||
await utils.registerEngine('isolated', createDummySelector, { contentScript: true });
|
||||
await utils.registerEngine(playwright, 'main', createDummySelector);
|
||||
await utils.registerEngine(playwright, 'isolated', createDummySelector, { contentScript: true });
|
||||
await page.setContent('<div><span><section></section></span></div>');
|
||||
await page.evaluate(() => window.__answer = document.querySelector('span'));
|
||||
// Works in main if asked.
|
||||
@ -803,7 +803,7 @@ describe.skip(USES_HOOKS)('selectors.register', () => {
|
||||
// Can be chained to css.
|
||||
expect(await page.$eval('main=ignored >> css=section', e => e.nodeName)).toBe('SECTION');
|
||||
});
|
||||
it('should handle errors', async ({page}) => {
|
||||
it('should handle errors', async ({playwright, page}) => {
|
||||
let error = await page.$('neverregister=ignored').catch(e => e);
|
||||
expect(error.message).toBe('Unknown engine "neverregister" while parsing selector neverregister=ignored');
|
||||
|
||||
@ -823,8 +823,8 @@ describe.skip(USES_HOOKS)('selectors.register', () => {
|
||||
expect(error.message).toBe('Selector engine name may only contain [a-zA-Z0-9_] characters');
|
||||
|
||||
// Selector names are case-sensitive.
|
||||
await utils.registerEngine('dummy', createDummySelector);
|
||||
await utils.registerEngine('duMMy', createDummySelector);
|
||||
await utils.registerEngine(playwright, 'dummy', createDummySelector);
|
||||
await utils.registerEngine(playwright, 'duMMy', createDummySelector);
|
||||
|
||||
error = await playwright.selectors.register('dummy', createDummySelector).catch(e => e);
|
||||
expect(error.message).toBe('"dummy" selector engine has been already registered');
|
||||
|
||||
@ -94,7 +94,7 @@ const utils = module.exports = {
|
||||
expect(await page.evaluate('window.innerHeight')).toBe(height);
|
||||
},
|
||||
|
||||
registerEngine: async (name, script, options) => {
|
||||
registerEngine: async (playwright, name, script, options) => {
|
||||
try {
|
||||
await playwright.selectors.register(name, script, options);
|
||||
} catch (e) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user