feat(rpc): support selectors (#2936)

This commit is contained in:
Dmitry Gozman 2020-07-13 17:47:15 -07:00 committed by GitHub
parent 6c75cbe5f5
commit 9fdb3e23c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 120 additions and 23 deletions

View File

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

View File

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

View File

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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