Skip to content

Commit 9fdb3e2

Browse files
authored
feat(rpc): support selectors (#2936)
1 parent 6c75cbe commit 9fdb3e2

11 files changed

+120
-23
lines changed

src/rpc/channels.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,17 @@ export type PlaywrightInitializer = {
3030
firefox: BrowserTypeChannel,
3131
webkit: BrowserTypeChannel,
3232
deviceDescriptors: types.Devices,
33+
selectors: SelectorsChannel,
3334
};
3435

3536

37+
export interface SelectorsChannel extends Channel {
38+
register(params: { name: string, source: string, options: { contentScript?: boolean } }): Promise<void>;
39+
createSelector(params: { name: string, handle: ElementHandleChannel }): Promise<string | undefined>;
40+
}
41+
export type SelectorsInitializer = {};
42+
43+
3644
export interface BrowserTypeChannel extends Channel {
3745
connect(params: types.ConnectOptions): Promise<BrowserChannel>;
3846
launch(params: types.LaunchOptions): Promise<BrowserChannel>;

src/rpc/client/connection.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { Playwright } from './playwright';
3535
import { Channel } from '../channels';
3636
import { ChromiumBrowser } from './chromiumBrowser';
3737
import { ChromiumBrowserContext } from './chromiumBrowserContext';
38+
import { Selectors } from './selectors';
3839

3940
class Root extends ChannelOwner<Channel, {}> {
4041
constructor(connection: Connection) {
@@ -190,6 +191,9 @@ export class Connection {
190191
case 'route':
191192
result = new Route(parent, type, guid, initializer);
192193
break;
194+
case 'selectors':
195+
result = new Selectors(parent, type, guid, initializer);
196+
break;
193197
case 'worker':
194198
result = new Worker(parent, type, guid, initializer);
195199
break;

src/rpc/client/playwright.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,21 @@ import { PlaywrightChannel, PlaywrightInitializer } from '../channels';
1818
import * as types from '../../types';
1919
import { BrowserType } from './browserType';
2020
import { ChannelOwner } from './channelOwner';
21+
import { Selectors } from './selectors';
2122

2223
export class Playwright extends ChannelOwner<PlaywrightChannel, PlaywrightInitializer> {
2324
chromium: BrowserType;
2425
firefox: BrowserType;
2526
webkit: BrowserType;
2627
devices: types.Devices;
28+
selectors: Selectors;
2729

2830
constructor(parent: ChannelOwner, type: string, guid: string, initializer: PlaywrightInitializer) {
2931
super(parent, type, guid, initializer);
3032
this.chromium = BrowserType.from(initializer.chromium);
3133
this.firefox = BrowserType.from(initializer.firefox);
3234
this.webkit = BrowserType.from(initializer.webkit);
3335
this.devices = initializer.deviceDescriptors;
36+
this.selectors = Selectors.from(initializer.selectors);
3437
}
3538
}

src/rpc/client/selectors.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* Copyright (c) Microsoft Corporation.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { SelectorsChannel, SelectorsInitializer } from '../channels';
18+
import { ChannelOwner } from './channelOwner';
19+
import { helper } from '../../helper';
20+
import { ElementHandle } from './elementHandle';
21+
22+
export class Selectors extends ChannelOwner<SelectorsChannel, SelectorsInitializer> {
23+
static from(selectors: SelectorsChannel): Selectors {
24+
return (selectors as any)._object;
25+
}
26+
27+
constructor(parent: ChannelOwner, type: string, guid: string, initializer: SelectorsInitializer) {
28+
super(parent, type, guid, initializer);
29+
}
30+
31+
async register(name: string, script: string | Function | { path?: string, content?: string }, options: { contentScript?: boolean } = {}): Promise<void> {
32+
const source = await helper.evaluationScript(script, undefined, false);
33+
await this._channel.register({ name, source, options });
34+
}
35+
36+
async _createSelector(name: string, handle: ElementHandle<Element>): Promise<string | undefined> {
37+
return this._channel.createSelector({ name, handle: handle._elementChannel });
38+
}
39+
}

src/rpc/server/playwrightDispatcher.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,16 @@ import { Playwright } from '../../server/playwright';
1818
import { PlaywrightChannel, PlaywrightInitializer } from '../channels';
1919
import { BrowserTypeDispatcher } from './browserTypeDispatcher';
2020
import { Dispatcher, DispatcherScope } from './dispatcher';
21+
import { SelectorsDispatcher } from './selectorsDispatcher';
2122

2223
export class PlaywrightDispatcher extends Dispatcher<Playwright, PlaywrightInitializer> implements PlaywrightChannel {
2324
constructor(scope: DispatcherScope, playwright: Playwright) {
2425
super(scope, playwright, 'playwright', {
2526
chromium: new BrowserTypeDispatcher(scope, playwright.chromium!),
2627
firefox: new BrowserTypeDispatcher(scope, playwright.firefox!),
2728
webkit: new BrowserTypeDispatcher(scope, playwright.webkit!),
28-
deviceDescriptors: playwright.devices
29+
deviceDescriptors: playwright.devices,
30+
selectors: new SelectorsDispatcher(scope, playwright.selectors),
2931
}, false, 'playwright');
3032
}
3133
}

src/rpc/server/selectorsDispatcher.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* Copyright (c) Microsoft Corporation.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the 'License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { Dispatcher, DispatcherScope } from './dispatcher';
18+
import { SelectorsInitializer, SelectorsChannel } from '../channels';
19+
import { Selectors } from '../../selectors';
20+
import { ElementHandleDispatcher } from './elementHandlerDispatcher';
21+
import * as dom from '../../dom';
22+
23+
export class SelectorsDispatcher extends Dispatcher<Selectors, SelectorsInitializer> implements SelectorsChannel {
24+
constructor(scope: DispatcherScope, selectors: Selectors) {
25+
super(scope, selectors, 'selectors', {});
26+
}
27+
28+
async register(params: { name: string, source: string, options: { contentScript?: boolean } }): Promise<void> {
29+
await this._object.register(params.name, params.source, params.options);
30+
}
31+
32+
async createSelector(params: { name: string, handle: ElementHandleDispatcher }): Promise<string | undefined> {
33+
return this._object._createSelector(params.name, params.handle._object as dom.ElementHandle<Element>);
34+
}
35+
}

test/channels.spec.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ describe.skip(!CHANNEL)('Channels', function() {
3232
{ _guid: 'browserType', objects: [] },
3333
{ _guid: 'browserType', objects: [] },
3434
{ _guid: 'playwright' },
35+
{ _guid: 'selectors' },
3536
]
3637
};
3738
await expectScopeState(browser, GOLDEN_PRECONDITION);
@@ -55,6 +56,7 @@ describe.skip(!CHANNEL)('Channels', function() {
5556
] },
5657
] },
5758
{ _guid: 'playwright' },
59+
{ _guid: 'selectors' },
5860
]
5961
});
6062

@@ -72,6 +74,7 @@ describe.skip(!CHANNEL)('Channels', function() {
7274
{ _guid: 'browserType', objects: [] },
7375
{ _guid: 'browserType', objects: [] },
7476
{ _guid: 'playwright' },
77+
{ _guid: 'selectors' },
7578
]
7679
};
7780
await expectScopeState(browserType, GOLDEN_PRECONDITION);
@@ -88,6 +91,7 @@ describe.skip(!CHANNEL)('Channels', function() {
8891
{ _guid: 'browserType', objects: [] },
8992
{ _guid: 'browserType', objects: [] },
9093
{ _guid: 'playwright' },
94+
{ _guid: 'selectors' },
9195
]
9296
});
9397

@@ -105,6 +109,7 @@ describe.skip(!CHANNEL)('Channels', function() {
105109
{ _guid: 'browserType', objects: [] },
106110
{ _guid: 'browserType', objects: [] },
107111
{ _guid: 'playwright' },
112+
{ _guid: 'selectors' },
108113
]
109114
};
110115
await expectScopeState(browserType, GOLDEN_PRECONDITION);
@@ -123,6 +128,7 @@ describe.skip(!CHANNEL)('Channels', function() {
123128
{ _guid: 'browserType', objects: [] },
124129
{ _guid: 'browserType', objects: [] },
125130
{ _guid: 'playwright' },
131+
{ _guid: 'selectors' },
126132
]
127133
});
128134

test/dispatchevent.spec.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ describe('Page.dispatchEvent(click)', function() {
9797
await watchdog;
9898
expect(await page.evaluate(() => window.clicked)).toBe(true);
9999
});
100-
it.skip(USES_HOOKS)('should be atomic', async({page}) => {
100+
it('should be atomic', async({playwright, page}) => {
101101
const createDummySelector = () => ({
102102
create(root, target) {},
103103
query(root, selector) {
@@ -113,7 +113,7 @@ describe('Page.dispatchEvent(click)', function() {
113113
return result;
114114
}
115115
});
116-
await utils.registerEngine('dispatchEvent', createDummySelector);
116+
await utils.registerEngine(playwright, 'dispatchEvent', createDummySelector);
117117
await page.setContent(`<div onclick="window._clicked=true">Hello</div>`);
118118
await page.dispatchEvent('dispatchEvent=div', 'click');
119119
expect(await page.evaluate(() => window._clicked)).toBe(true);

test/elementhandle.spec.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -484,7 +484,7 @@ describe('ElementHandle convenience API', function() {
484484
expect(await handle.textContent()).toBe('Text,\nmore text');
485485
expect(await page.textContent('#inner')).toBe('Text,\nmore text');
486486
});
487-
it.skip(USES_HOOKS)('textContent should be atomic', async({page}) => {
487+
it('textContent should be atomic', async({playwright, page}) => {
488488
const createDummySelector = () => ({
489489
create(root, target) {},
490490
query(root, selector) {
@@ -500,13 +500,13 @@ describe('ElementHandle convenience API', function() {
500500
return result;
501501
}
502502
});
503-
await utils.registerEngine('textContent', createDummySelector);
503+
await utils.registerEngine(playwright, 'textContent', createDummySelector);
504504
await page.setContent(`<div>Hello</div>`);
505505
const tc = await page.textContent('textContent=div');
506506
expect(tc).toBe('Hello');
507507
expect(await page.evaluate(() => document.querySelector('div').textContent)).toBe('modified');
508508
});
509-
it.skip(USES_HOOKS)('innerText should be atomic', async({page}) => {
509+
it('innerText should be atomic', async({playwright, page}) => {
510510
const createDummySelector = () => ({
511511
create(root, target) {},
512512
query(root, selector) {
@@ -522,13 +522,13 @@ describe('ElementHandle convenience API', function() {
522522
return result;
523523
}
524524
});
525-
await utils.registerEngine('innerText', createDummySelector);
525+
await utils.registerEngine(playwright, 'innerText', createDummySelector);
526526
await page.setContent(`<div>Hello</div>`);
527527
const tc = await page.innerText('innerText=div');
528528
expect(tc).toBe('Hello');
529529
expect(await page.evaluate(() => document.querySelector('div').innerText)).toBe('modified');
530530
});
531-
it.skip(USES_HOOKS)('innerHTML should be atomic', async({page}) => {
531+
it('innerHTML should be atomic', async({playwright, page}) => {
532532
const createDummySelector = () => ({
533533
create(root, target) {},
534534
query(root, selector) {
@@ -544,13 +544,13 @@ describe('ElementHandle convenience API', function() {
544544
return result;
545545
}
546546
});
547-
await utils.registerEngine('innerHTML', createDummySelector);
547+
await utils.registerEngine(playwright, 'innerHTML', createDummySelector);
548548
await page.setContent(`<div>Hello<span>world</span></div>`);
549549
const tc = await page.innerHTML('innerHTML=div');
550550
expect(tc).toBe('Hello<span>world</span>');
551551
expect(await page.evaluate(() => document.querySelector('div').innerHTML)).toBe('modified');
552552
});
553-
it.skip(USES_HOOKS)('getAttribute should be atomic', async({page}) => {
553+
it('getAttribute should be atomic', async({playwright, page}) => {
554554
const createDummySelector = () => ({
555555
create(root, target) {},
556556
query(root, selector) {
@@ -566,7 +566,7 @@ describe('ElementHandle convenience API', function() {
566566
return result;
567567
}
568568
});
569-
await utils.registerEngine('getAttribute', createDummySelector);
569+
await utils.registerEngine(playwright, 'getAttribute', createDummySelector);
570570
await page.setContent(`<div foo=hello></div>`);
571571
const tc = await page.getAttribute('getAttribute=div', 'foo');
572572
expect(tc).toBe('hello');

test/queryselector.spec.js

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -743,8 +743,8 @@ describe('attribute selector', () => {
743743
});
744744
});
745745

746-
describe.skip(USES_HOOKS)('selectors.register', () => {
747-
it.skip(CHANNEL)('should work', async ({page}) => {
746+
describe('selectors.register', () => {
747+
it('should work', async ({playwright, page}) => {
748748
const createTagSelector = () => ({
749749
create(root, target) {
750750
return target.nodeName;
@@ -756,7 +756,7 @@ describe.skip(USES_HOOKS)('selectors.register', () => {
756756
return Array.from(root.querySelectorAll(selector));
757757
}
758758
});
759-
await utils.registerEngine('tag', `(${createTagSelector.toString()})()`);
759+
await utils.registerEngine(playwright, 'tag', `(${createTagSelector.toString()})()`);
760760
await page.setContent('<div><span></span></div><div></div>');
761761
expect(await playwright.selectors._createSelector('tag', await page.$('div'))).toBe('DIV');
762762
expect(await page.$eval('tag=DIV', e => e.nodeName)).toBe('DIV');
@@ -767,12 +767,12 @@ describe.skip(USES_HOOKS)('selectors.register', () => {
767767
const error = await page.$('tAG=DIV').catch(e => e);
768768
expect(error.message).toBe('Unknown engine "tAG" while parsing selector tAG=DIV');
769769
});
770-
it('should work with path', async ({page}) => {
771-
await utils.registerEngine('foo', { path: path.join(__dirname, 'assets/sectionselectorengine.js') });
770+
it('should work with path', async ({playwright, page}) => {
771+
await utils.registerEngine(playwright, 'foo', { path: path.join(__dirname, 'assets/sectionselectorengine.js') });
772772
await page.setContent('<section></section>');
773773
expect(await page.$eval('foo=whatever', e => e.nodeName)).toBe('SECTION');
774774
});
775-
it('should work in main and isolated world', async ({page}) => {
775+
it('should work in main and isolated world', async ({playwright, page}) => {
776776
const createDummySelector = () => ({
777777
create(root, target) { },
778778
query(root, selector) {
@@ -782,8 +782,8 @@ describe.skip(USES_HOOKS)('selectors.register', () => {
782782
return [document.body, document.documentElement, window.__answer];
783783
}
784784
});
785-
await utils.registerEngine('main', createDummySelector);
786-
await utils.registerEngine('isolated', createDummySelector, { contentScript: true });
785+
await utils.registerEngine(playwright, 'main', createDummySelector);
786+
await utils.registerEngine(playwright, 'isolated', createDummySelector, { contentScript: true });
787787
await page.setContent('<div><span><section></section></span></div>');
788788
await page.evaluate(() => window.__answer = document.querySelector('span'));
789789
// Works in main if asked.
@@ -803,7 +803,7 @@ describe.skip(USES_HOOKS)('selectors.register', () => {
803803
// Can be chained to css.
804804
expect(await page.$eval('main=ignored >> css=section', e => e.nodeName)).toBe('SECTION');
805805
});
806-
it('should handle errors', async ({page}) => {
806+
it('should handle errors', async ({playwright, page}) => {
807807
let error = await page.$('neverregister=ignored').catch(e => e);
808808
expect(error.message).toBe('Unknown engine "neverregister" while parsing selector neverregister=ignored');
809809

@@ -823,8 +823,8 @@ describe.skip(USES_HOOKS)('selectors.register', () => {
823823
expect(error.message).toBe('Selector engine name may only contain [a-zA-Z0-9_] characters');
824824

825825
// Selector names are case-sensitive.
826-
await utils.registerEngine('dummy', createDummySelector);
827-
await utils.registerEngine('duMMy', createDummySelector);
826+
await utils.registerEngine(playwright, 'dummy', createDummySelector);
827+
await utils.registerEngine(playwright, 'duMMy', createDummySelector);
828828

829829
error = await playwright.selectors.register('dummy', createDummySelector).catch(e => e);
830830
expect(error.message).toBe('"dummy" selector engine has been already registered');

0 commit comments

Comments
 (0)