From 9fdb3e23c3d7389bec96d9384710025cfbd99335 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Mon, 13 Jul 2020 17:47:15 -0700 Subject: [PATCH] feat(rpc): support selectors (#2936) --- src/rpc/channels.ts | 8 ++++++ src/rpc/client/connection.ts | 4 +++ src/rpc/client/playwright.ts | 3 ++ src/rpc/client/selectors.ts | 39 ++++++++++++++++++++++++++ src/rpc/server/playwrightDispatcher.ts | 4 ++- src/rpc/server/selectorsDispatcher.ts | 35 +++++++++++++++++++++++ test/channels.spec.js | 6 ++++ test/dispatchevent.spec.js | 4 +-- test/elementhandle.spec.js | 16 +++++------ test/queryselector.spec.js | 22 +++++++-------- test/utils.js | 2 +- 11 files changed, 120 insertions(+), 23 deletions(-) create mode 100644 src/rpc/client/selectors.ts create mode 100644 src/rpc/server/selectorsDispatcher.ts diff --git a/src/rpc/channels.ts b/src/rpc/channels.ts index 930781fdf8..a7ef1d6c30 100644 --- a/src/rpc/channels.ts +++ b/src/rpc/channels.ts @@ -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; + createSelector(params: { name: string, handle: ElementHandleChannel }): Promise; +} +export type SelectorsInitializer = {}; + + export interface BrowserTypeChannel extends Channel { connect(params: types.ConnectOptions): Promise; launch(params: types.LaunchOptions): Promise; diff --git a/src/rpc/client/connection.ts b/src/rpc/client/connection.ts index cdd51e29ba..8b38528a20 100644 --- a/src/rpc/client/connection.ts +++ b/src/rpc/client/connection.ts @@ -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 { 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; diff --git a/src/rpc/client/playwright.ts b/src/rpc/client/playwright.ts index d6fd467753..edfab0a08d 100644 --- a/src/rpc/client/playwright.ts +++ b/src/rpc/client/playwright.ts @@ -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 { 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 { + 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 { + const source = await helper.evaluationScript(script, undefined, false); + await this._channel.register({ name, source, options }); + } + + async _createSelector(name: string, handle: ElementHandle): Promise { + return this._channel.createSelector({ name, handle: handle._elementChannel }); + } +} diff --git a/src/rpc/server/playwrightDispatcher.ts b/src/rpc/server/playwrightDispatcher.ts index 94791ca64c..4176a68f3d 100644 --- a/src/rpc/server/playwrightDispatcher.ts +++ b/src/rpc/server/playwrightDispatcher.ts @@ -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 implements PlaywrightChannel { constructor(scope: DispatcherScope, playwright: Playwright) { @@ -25,7 +26,8 @@ export class PlaywrightDispatcher extends Dispatcher implements SelectorsChannel { + constructor(scope: DispatcherScope, selectors: Selectors) { + super(scope, selectors, 'selectors', {}); + } + + async register(params: { name: string, source: string, options: { contentScript?: boolean } }): Promise { + await this._object.register(params.name, params.source, params.options); + } + + async createSelector(params: { name: string, handle: ElementHandleDispatcher }): Promise { + return this._object._createSelector(params.name, params.handle._object as dom.ElementHandle); + } +} diff --git a/test/channels.spec.js b/test/channels.spec.js index 9057e8fbc3..5b0c797a5b 100644 --- a/test/channels.spec.js +++ b/test/channels.spec.js @@ -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' }, ] }); diff --git a/test/dispatchevent.spec.js b/test/dispatchevent.spec.js index 2fcfa4a415..d62ee21c8c 100644 --- a/test/dispatchevent.spec.js +++ b/test/dispatchevent.spec.js @@ -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(`
Hello
`); await page.dispatchEvent('dispatchEvent=div', 'click'); expect(await page.evaluate(() => window._clicked)).toBe(true); diff --git a/test/elementhandle.spec.js b/test/elementhandle.spec.js index c03cc3a71c..f9e563f59e 100644 --- a/test/elementhandle.spec.js +++ b/test/elementhandle.spec.js @@ -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(`
Hello
`); 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(`
Hello
`); 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(`
Helloworld
`); const tc = await page.innerHTML('innerHTML=div'); expect(tc).toBe('Helloworld'); 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(`
`); const tc = await page.getAttribute('getAttribute=div', 'foo'); expect(tc).toBe('hello'); diff --git a/test/queryselector.spec.js b/test/queryselector.spec.js index cbb69ed364..23ce695d66 100644 --- a/test/queryselector.spec.js +++ b/test/queryselector.spec.js @@ -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('
'); 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('
'); 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('
'); 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'); diff --git a/test/utils.js b/test/utils.js index 8fae22c599..9ea31ce3db 100644 --- a/test/utils.js +++ b/test/utils.js @@ -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) {