fix(connect): make selectors.register work in connected browser (#3664)
This is a large rework of selectors: - Each BrowserContext now has a separate Selectors instance that has its own registrations. Most of them share a single sharedSelectors instance, but contexts created for a connected browser have their own instance. - Connected browser now gets a RemoteBrowser object that encapsulates Selectors and Browser. This Selectors object is registered with the api selectors. - Public selectors.register api iterates over all registered Selectors channels and registers in each of them. - createSelector testing method migrated to ElementHandle._createSelectorForTest.
This commit is contained in:
parent
5c3bf5bf3e
commit
de547d7d65
|
|
@ -20,13 +20,15 @@ import * as ws from 'ws';
|
|||
import { Browser } from './server/browser';
|
||||
import { ChildProcess } from 'child_process';
|
||||
import { EventEmitter } from 'ws';
|
||||
import { DispatcherScope, DispatcherConnection } from './dispatchers/dispatcher';
|
||||
import { Dispatcher, DispatcherScope, DispatcherConnection } from './dispatchers/dispatcher';
|
||||
import { BrowserDispatcher } from './dispatchers/browserDispatcher';
|
||||
import { BrowserContextDispatcher } from './dispatchers/browserContextDispatcher';
|
||||
import { BrowserNewContextParams, BrowserContextChannel } from './protocol/channels';
|
||||
import * as channels from './protocol/channels';
|
||||
import { BrowserServerLauncher, BrowserServer } from './client/browserType';
|
||||
import { envObjectToArray } from './client/clientHelper';
|
||||
import { createGuid } from './utils/utils';
|
||||
import { SelectorsDispatcher } from './dispatchers/selectorsDispatcher';
|
||||
import { Selectors } from './server/selectors';
|
||||
|
||||
export class BrowserServerLauncherImpl implements BrowserServerLauncher {
|
||||
private _browserType: BrowserTypeBase;
|
||||
|
|
@ -105,7 +107,10 @@ export class BrowserServerImpl extends EventEmitter implements BrowserServer {
|
|||
connection.dispatch(JSON.parse(Buffer.from(message).toString()));
|
||||
});
|
||||
socket.on('error', () => {});
|
||||
const browser = new ConnectedBrowser(connection.rootDispatcher(), this._browser);
|
||||
const selectors = new Selectors();
|
||||
const scope = connection.rootDispatcher();
|
||||
const browser = new ConnectedBrowser(scope, this._browser, selectors);
|
||||
new RemoteBrowserDispatcher(scope, browser, selectors);
|
||||
socket.on('close', () => {
|
||||
// Avoid sending any more messages over closed socket.
|
||||
connection.onmessage = () => {};
|
||||
|
|
@ -115,17 +120,30 @@ export class BrowserServerImpl extends EventEmitter implements BrowserServer {
|
|||
}
|
||||
}
|
||||
|
||||
class RemoteBrowserDispatcher extends Dispatcher<{}, channels.RemoteBrowserInitializer> implements channels.PlaywrightChannel {
|
||||
constructor(scope: DispatcherScope, browser: ConnectedBrowser, selectors: Selectors) {
|
||||
super(scope, {}, 'RemoteBrowser', {
|
||||
selectors: new SelectorsDispatcher(scope, selectors),
|
||||
browser,
|
||||
}, false, 'remoteBrowser');
|
||||
}
|
||||
}
|
||||
|
||||
class ConnectedBrowser extends BrowserDispatcher {
|
||||
private _contexts: BrowserContextDispatcher[] = [];
|
||||
private _selectors: Selectors;
|
||||
_closed = false;
|
||||
|
||||
constructor(scope: DispatcherScope, browser: Browser) {
|
||||
super(scope, browser, 'connectedBrowser');
|
||||
constructor(scope: DispatcherScope, browser: Browser, selectors: Selectors) {
|
||||
super(scope, browser);
|
||||
this._selectors = selectors;
|
||||
}
|
||||
|
||||
async newContext(params: BrowserNewContextParams): Promise<{ context: BrowserContextChannel }> {
|
||||
async newContext(params: channels.BrowserNewContextParams): Promise<{ context: channels.BrowserContextChannel }> {
|
||||
const result = await super.newContext(params);
|
||||
this._contexts.push(result.context as BrowserContextDispatcher);
|
||||
const dispatcher = result.context as BrowserContextDispatcher;
|
||||
dispatcher._object._setSelectors(this._selectors);
|
||||
this._contexts.push(dispatcher);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import { ChildProcess } from 'child_process';
|
|||
import { envObjectToArray } from './clientHelper';
|
||||
import { validateHeaders } from './network';
|
||||
import { assert, makeWaitForNextTask, headersObjectToArray } from '../utils/utils';
|
||||
import { SelectorsOwner, sharedSelectors } from './selectors';
|
||||
|
||||
export interface BrowserServerLauncher {
|
||||
launchServer(options?: LaunchServerOptions): Promise<BrowserServer>;
|
||||
|
|
@ -144,7 +145,13 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel, chann
|
|||
}
|
||||
}
|
||||
ws.addEventListener('open', async () => {
|
||||
const browser = (await connection.waitForObjectWithKnownName('connectedBrowser')) as Browser;
|
||||
const remoteBrowser = await connection.waitForObjectWithKnownName('remoteBrowser') as RemoteBrowser;
|
||||
|
||||
// Inherit shared selectors for connected browser.
|
||||
const selectorsOwner = SelectorsOwner.from(remoteBrowser._initializer.selectors);
|
||||
sharedSelectors._addChannel(selectorsOwner);
|
||||
|
||||
const browser = Browser.from(remoteBrowser._initializer.browser);
|
||||
browser._logger = logger;
|
||||
browser._isRemote = true;
|
||||
const closeListener = () => {
|
||||
|
|
@ -158,6 +165,7 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel, chann
|
|||
};
|
||||
ws.addEventListener('close', closeListener);
|
||||
browser.on(Events.Browser.Disconnected, () => {
|
||||
sharedSelectors._removeChannel(selectorsOwner);
|
||||
ws.removeEventListener('close', closeListener);
|
||||
ws.close();
|
||||
});
|
||||
|
|
@ -171,3 +179,6 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel, chann
|
|||
}, logger);
|
||||
}
|
||||
}
|
||||
|
||||
export class RemoteBrowser extends ChannelOwner<channels.RemoteBrowserChannel, channels.RemoteBrowserInitializer> {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
import { Browser } from './browser';
|
||||
import { BrowserContext } from './browserContext';
|
||||
import { BrowserType } from './browserType';
|
||||
import { BrowserType, RemoteBrowser } from './browserType';
|
||||
import { ChannelOwner } from './channelOwner';
|
||||
import { ElementHandle } from './elementHandle';
|
||||
import { Frame } from './frame';
|
||||
|
|
@ -34,12 +34,12 @@ import { Electron, ElectronApplication } from './electron';
|
|||
import * as channels from '../protocol/channels';
|
||||
import { ChromiumBrowser } from './chromiumBrowser';
|
||||
import { ChromiumBrowserContext } from './chromiumBrowserContext';
|
||||
import { Selectors } from './selectors';
|
||||
import { Stream } from './stream';
|
||||
import { createScheme, Validator, ValidationError } from '../protocol/validator';
|
||||
import { WebKitBrowser } from './webkitBrowser';
|
||||
import { FirefoxBrowser } from './firefoxBrowser';
|
||||
import { debugLogger } from '../utils/debugLogger';
|
||||
import { SelectorsOwner } from './selectors';
|
||||
|
||||
class Root extends ChannelOwner<channels.Channel, {}> {
|
||||
constructor(connection: Connection) {
|
||||
|
|
@ -195,20 +195,23 @@ export class Connection {
|
|||
case 'Playwright':
|
||||
result = new Playwright(parent, type, guid, initializer);
|
||||
break;
|
||||
case 'RemoteBrowser':
|
||||
result = new RemoteBrowser(parent, type, guid, initializer);
|
||||
break;
|
||||
case 'Request':
|
||||
result = new Request(parent, type, guid, initializer);
|
||||
break;
|
||||
case 'Stream':
|
||||
result = new Stream(parent, type, guid, initializer);
|
||||
break;
|
||||
case 'Response':
|
||||
result = new Response(parent, type, guid, initializer);
|
||||
break;
|
||||
case 'Route':
|
||||
result = new Route(parent, type, guid, initializer);
|
||||
break;
|
||||
case 'Stream':
|
||||
result = new Stream(parent, type, guid, initializer);
|
||||
break;
|
||||
case 'Selectors':
|
||||
result = new Selectors(parent, type, guid, initializer);
|
||||
result = new SelectorsOwner(parent, type, guid, initializer);
|
||||
break;
|
||||
case 'Worker':
|
||||
result = new Worker(parent, type, guid, initializer);
|
||||
|
|
|
|||
|
|
@ -233,6 +233,10 @@ export class ElementHandle<T extends Node = Node> extends JSHandle<T> {
|
|||
return ElementHandle.fromNullable(result.element) as ElementHandle<Element> | null;
|
||||
});
|
||||
}
|
||||
|
||||
async _createSelectorForTest(name: string): Promise<string | undefined> {
|
||||
return (await this._elementChannel.createSelectorForTest({ name })).value;
|
||||
}
|
||||
}
|
||||
|
||||
export function convertSelectOptionValues(values: string | ElementHandle | SelectOption | string[] | ElementHandle[] | SelectOption[] | null): { elements?: channels.ElementHandleChannel[], options?: SelectOption[] } {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
import * as channels from '../protocol/channels';
|
||||
import { BrowserType } from './browserType';
|
||||
import { ChannelOwner } from './channelOwner';
|
||||
import { Selectors } from './selectors';
|
||||
import { Selectors, SelectorsOwner, sharedSelectors } from './selectors';
|
||||
import { Electron } from './electron';
|
||||
import { TimeoutError } from '../utils/errors';
|
||||
import { Size } from './types';
|
||||
|
|
@ -49,7 +49,8 @@ export class Playwright extends ChannelOwner<channels.PlaywrightChannel, channel
|
|||
this.devices = {};
|
||||
for (const { name, descriptor } of initializer.deviceDescriptors)
|
||||
this.devices[name] = descriptor;
|
||||
this.selectors = Selectors.from(initializer.selectors);
|
||||
this.selectors = sharedSelectors;
|
||||
this.errors = { TimeoutError };
|
||||
this.selectors._addChannel(SelectorsOwner.from(initializer.selectors));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,26 +14,39 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { evaluationScript } from './clientHelper';
|
||||
import * as channels from '../protocol/channels';
|
||||
import { ChannelOwner } from './channelOwner';
|
||||
import { ElementHandle } from './elementHandle';
|
||||
import { evaluationScript } from './clientHelper';
|
||||
|
||||
export class Selectors extends ChannelOwner<channels.SelectorsChannel, channels.SelectorsInitializer> {
|
||||
static from(selectors: channels.SelectorsChannel): Selectors {
|
||||
return (selectors as any)._object;
|
||||
}
|
||||
|
||||
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.SelectorsInitializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
}
|
||||
export class Selectors {
|
||||
private _channels = new Set<SelectorsOwner>();
|
||||
private _registrations: channels.SelectorsRegisterParams[] = [];
|
||||
|
||||
async register(name: string, script: string | Function | { path?: string, content?: string }, options: { contentScript?: boolean } = {}): Promise<void> {
|
||||
const source = await evaluationScript(script, undefined, false);
|
||||
await this._channel.register({ ...options, name, source });
|
||||
const params = { ...options, name, source };
|
||||
for (const channel of this._channels)
|
||||
await channel._channel.register(params);
|
||||
this._registrations.push(params);
|
||||
}
|
||||
|
||||
async _createSelector(name: string, handle: ElementHandle<Element>): Promise<string | undefined> {
|
||||
return (await this._channel.createSelector({ name, handle: handle._elementChannel })).value;
|
||||
_addChannel(channel: SelectorsOwner) {
|
||||
this._channels.add(channel);
|
||||
for (const params of this._registrations) {
|
||||
// This should not fail except for connection closure, but just in case we catch.
|
||||
channel._channel.register(params).catch(e => {});
|
||||
}
|
||||
}
|
||||
|
||||
_removeChannel(channel: SelectorsOwner) {
|
||||
this._channels.delete(channel);
|
||||
}
|
||||
}
|
||||
|
||||
export class SelectorsOwner extends ChannelOwner<channels.SelectorsChannel, channels.SelectorsInitializer> {
|
||||
static from(browser: channels.SelectorsChannel): SelectorsOwner {
|
||||
return (browser as any)._object;
|
||||
}
|
||||
}
|
||||
|
||||
export const sharedSelectors = new Selectors();
|
||||
|
|
|
|||
|
|
@ -23,8 +23,8 @@ import { CRBrowser } from '../server/chromium/crBrowser';
|
|||
import { PageDispatcher } from './pageDispatcher';
|
||||
|
||||
export class BrowserDispatcher extends Dispatcher<Browser, channels.BrowserInitializer> implements channels.BrowserChannel {
|
||||
constructor(scope: DispatcherScope, browser: Browser, guid?: string) {
|
||||
super(scope, browser, 'Browser', { version: browser.version(), name: browser._options.name }, true, guid);
|
||||
constructor(scope: DispatcherScope, browser: Browser) {
|
||||
super(scope, browser, 'Browser', { version: browser.version(), name: browser._options.name }, true);
|
||||
browser.on(Browser.Events.Disconnected, () => this._didClose());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -156,4 +156,8 @@ export class ElementHandleDispatcher extends JSHandleDispatcher implements chann
|
|||
async waitForSelector(params: channels.ElementHandleWaitForSelectorParams): Promise<channels.ElementHandleWaitForSelectorResult> {
|
||||
return { element: ElementHandleDispatcher.createNullable(this._scope, await this._elementHandle.waitForSelector(params.selector, params)) };
|
||||
}
|
||||
|
||||
async createSelectorForTest(params: channels.ElementHandleCreateSelectorForTestParams): Promise<channels.ElementHandleCreateSelectorForTestResult> {
|
||||
return { value: await this._elementHandle._page.selectors._createSelector(params.name, this._elementHandle as ElementHandle<Element>) };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,10 +18,10 @@ import { Playwright } from '../server/playwright';
|
|||
import * as channels from '../protocol/channels';
|
||||
import { BrowserTypeDispatcher } from './browserTypeDispatcher';
|
||||
import { Dispatcher, DispatcherScope } from './dispatcher';
|
||||
import { SelectorsDispatcher } from './selectorsDispatcher';
|
||||
import { Electron } from '../server/electron/electron';
|
||||
import { ElectronDispatcher } from './electronDispatcher';
|
||||
import { DeviceDescriptors } from '../server/deviceDescriptors';
|
||||
import { SelectorsDispatcher } from './selectorsDispatcher';
|
||||
|
||||
export class PlaywrightDispatcher extends Dispatcher<Playwright, channels.PlaywrightInitializer> implements channels.PlaywrightChannel {
|
||||
constructor(scope: DispatcherScope, playwright: Playwright) {
|
||||
|
|
|
|||
|
|
@ -17,8 +17,6 @@
|
|||
import { Dispatcher, DispatcherScope } from './dispatcher';
|
||||
import * as channels from '../protocol/channels';
|
||||
import { Selectors } from '../server/selectors';
|
||||
import { ElementHandleDispatcher } from './elementHandlerDispatcher';
|
||||
import * as dom from '../server/dom';
|
||||
|
||||
export class SelectorsDispatcher extends Dispatcher<Selectors, channels.SelectorsInitializer> implements channels.SelectorsChannel {
|
||||
constructor(scope: DispatcherScope, selectors: Selectors) {
|
||||
|
|
@ -28,8 +26,4 @@ export class SelectorsDispatcher extends Dispatcher<Selectors, channels.Selector
|
|||
async register(params: channels.SelectorsRegisterParams): Promise<void> {
|
||||
await this._object.register(params.name, params.source, params.contentScript);
|
||||
}
|
||||
|
||||
async createSelector(params: channels.SelectorsCreateSelectorParams): Promise<channels.SelectorsCreateSelectorResult> {
|
||||
return { value: await this._object._createSelector(params.name, (params.handle as ElementHandleDispatcher)._object as dom.ElementHandle<Element>) };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,11 +109,18 @@ export type PlaywrightInitializer = {
|
|||
export interface PlaywrightChannel extends Channel {
|
||||
}
|
||||
|
||||
// ----------- RemoteBrowser -----------
|
||||
export type RemoteBrowserInitializer = {
|
||||
browser: BrowserChannel,
|
||||
selectors: SelectorsChannel,
|
||||
};
|
||||
export interface RemoteBrowserChannel extends Channel {
|
||||
}
|
||||
|
||||
// ----------- Selectors -----------
|
||||
export type SelectorsInitializer = {};
|
||||
export interface SelectorsChannel extends Channel {
|
||||
register(params: SelectorsRegisterParams): Promise<SelectorsRegisterResult>;
|
||||
createSelector(params: SelectorsCreateSelectorParams): Promise<SelectorsCreateSelectorResult>;
|
||||
}
|
||||
export type SelectorsRegisterParams = {
|
||||
name: string,
|
||||
|
|
@ -124,16 +131,6 @@ export type SelectorsRegisterOptions = {
|
|||
contentScript?: boolean,
|
||||
};
|
||||
export type SelectorsRegisterResult = void;
|
||||
export type SelectorsCreateSelectorParams = {
|
||||
name: string,
|
||||
handle: ElementHandleChannel,
|
||||
};
|
||||
export type SelectorsCreateSelectorOptions = {
|
||||
|
||||
};
|
||||
export type SelectorsCreateSelectorResult = {
|
||||
value?: string,
|
||||
};
|
||||
|
||||
// ----------- BrowserType -----------
|
||||
export type BrowserTypeInitializer = {
|
||||
|
|
@ -1625,6 +1622,7 @@ export interface ElementHandleChannel extends JSHandleChannel {
|
|||
uncheck(params: ElementHandleUncheckParams): Promise<ElementHandleUncheckResult>;
|
||||
waitForElementState(params: ElementHandleWaitForElementStateParams): Promise<ElementHandleWaitForElementStateResult>;
|
||||
waitForSelector(params: ElementHandleWaitForSelectorParams): Promise<ElementHandleWaitForSelectorResult>;
|
||||
createSelectorForTest(params: ElementHandleCreateSelectorForTestParams): Promise<ElementHandleCreateSelectorForTestResult>;
|
||||
}
|
||||
export type ElementHandleEvalOnSelectorParams = {
|
||||
selector: string,
|
||||
|
|
@ -1936,6 +1934,15 @@ export type ElementHandleWaitForSelectorOptions = {
|
|||
export type ElementHandleWaitForSelectorResult = {
|
||||
element?: ElementHandleChannel,
|
||||
};
|
||||
export type ElementHandleCreateSelectorForTestParams = {
|
||||
name: string,
|
||||
};
|
||||
export type ElementHandleCreateSelectorForTestOptions = {
|
||||
|
||||
};
|
||||
export type ElementHandleCreateSelectorForTestResult = {
|
||||
value?: string,
|
||||
};
|
||||
|
||||
// ----------- Request -----------
|
||||
export type RequestInitializer = {
|
||||
|
|
|
|||
|
|
@ -146,6 +146,13 @@ Playwright:
|
|||
selectors: Selectors
|
||||
|
||||
|
||||
RemoteBrowser:
|
||||
type: interface
|
||||
|
||||
initializer:
|
||||
browser: Browser
|
||||
selectors: Selectors
|
||||
|
||||
|
||||
Selectors:
|
||||
type: interface
|
||||
|
|
@ -158,14 +165,6 @@ Selectors:
|
|||
source: string
|
||||
contentScript: boolean?
|
||||
|
||||
createSelector:
|
||||
parameters:
|
||||
name: string
|
||||
handle: ElementHandle
|
||||
returns:
|
||||
value: string?
|
||||
|
||||
|
||||
|
||||
BrowserType:
|
||||
type: interface
|
||||
|
|
@ -1606,6 +1605,12 @@ ElementHandle:
|
|||
returns:
|
||||
element: ElementHandle?
|
||||
|
||||
createSelectorForTest:
|
||||
parameters:
|
||||
name: string
|
||||
returns:
|
||||
value: string?
|
||||
|
||||
|
||||
Request:
|
||||
type: interface
|
||||
|
|
|
|||
|
|
@ -96,10 +96,6 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
|||
source: tString,
|
||||
contentScript: tOptional(tBoolean),
|
||||
});
|
||||
scheme.SelectorsCreateSelectorParams = tObject({
|
||||
name: tString,
|
||||
handle: tChannel('ElementHandle'),
|
||||
});
|
||||
scheme.BrowserTypeLaunchParams = tObject({
|
||||
executablePath: tOptional(tString),
|
||||
args: tOptional(tArray(tString)),
|
||||
|
|
@ -767,6 +763,9 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
|||
timeout: tOptional(tNumber),
|
||||
state: tOptional(tEnum(['attached', 'detached', 'visible', 'hidden'])),
|
||||
});
|
||||
scheme.ElementHandleCreateSelectorForTestParams = tObject({
|
||||
name: tString,
|
||||
});
|
||||
scheme.RequestResponseParams = tOptional(tObject({}));
|
||||
scheme.RouteAbortParams = tObject({
|
||||
errorCode: tOptional(tString),
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import { Progress } from './progress';
|
|||
import { DebugController } from './debug/debugController';
|
||||
import { isDebugMode } from '../utils/utils';
|
||||
import { Snapshotter, SnapshotterDelegate } from './snapshotter';
|
||||
import { Selectors, serverSelectors } from './selectors';
|
||||
|
||||
export class Screencast {
|
||||
readonly page: Page;
|
||||
|
|
@ -68,6 +69,7 @@ export abstract class BrowserContext extends EventEmitter {
|
|||
readonly _downloads = new Set<Download>();
|
||||
readonly _browser: Browser;
|
||||
readonly _browserContextId: string | undefined;
|
||||
private _selectors?: Selectors;
|
||||
_snapshotter?: Snapshotter;
|
||||
|
||||
constructor(browser: Browser, options: types.BrowserContextOptions, browserContextId: string | undefined) {
|
||||
|
|
@ -79,6 +81,14 @@ export abstract class BrowserContext extends EventEmitter {
|
|||
this._closePromise = new Promise(fulfill => this._closePromiseFulfill = fulfill);
|
||||
}
|
||||
|
||||
_setSelectors(selectors: Selectors) {
|
||||
this._selectors = selectors;
|
||||
}
|
||||
|
||||
selectors() {
|
||||
return this._selectors || serverSelectors;
|
||||
}
|
||||
|
||||
async _initialize() {
|
||||
if (isDebugMode())
|
||||
new DebugController(this);
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import * as injectedScriptSource from '../generated/injectedScriptSource';
|
|||
import * as debugScriptSource from '../generated/debugScriptSource';
|
||||
import * as js from './javascript';
|
||||
import { Page } from './page';
|
||||
import { selectors, SelectorInfo } from './selectors';
|
||||
import { SelectorInfo } from './selectors';
|
||||
import * as types from './types';
|
||||
import { Progress } from './progress';
|
||||
import type DebugScript from './debug/injected/debugScript';
|
||||
|
|
@ -80,7 +80,7 @@ export class FrameExecutionContext extends js.ExecutionContext {
|
|||
injectedScript(): Promise<js.JSHandle<InjectedScript>> {
|
||||
if (!this._injectedScriptPromise) {
|
||||
const custom: string[] = [];
|
||||
for (const [name, { source }] of selectors._engines)
|
||||
for (const [name, { source }] of this.frame._page.selectors._engines)
|
||||
custom.push(`{ name: '${name}', engine: (${source}) }`);
|
||||
const source = `
|
||||
new (${injectedScriptSource.source})([
|
||||
|
|
@ -580,15 +580,15 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
|||
}
|
||||
|
||||
async $(selector: string): Promise<ElementHandle | null> {
|
||||
return selectors._query(this._context.frame, selector, this);
|
||||
return this._page.selectors._query(this._context.frame, selector, this);
|
||||
}
|
||||
|
||||
async $$(selector: string): Promise<ElementHandle<Element>[]> {
|
||||
return selectors._queryAll(this._context.frame, selector, this);
|
||||
return this._page.selectors._queryAll(this._context.frame, selector, this);
|
||||
}
|
||||
|
||||
async _$evalExpression(selector: string, expression: string, isFunction: boolean, arg: any): Promise<any> {
|
||||
const handle = await selectors._query(this._context.frame, selector, this);
|
||||
const handle = await this._page.selectors._query(this._context.frame, selector, this);
|
||||
if (!handle)
|
||||
throw new Error(`Error: failed to find element matching selector "${selector}"`);
|
||||
const result = await handle._evaluateExpression(expression, isFunction, true, arg);
|
||||
|
|
@ -597,7 +597,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
|||
}
|
||||
|
||||
async _$$evalExpression(selector: string, expression: string, isFunction: boolean, arg: any): Promise<any> {
|
||||
const arrayHandle = await selectors._queryArray(this._context.frame, selector, this);
|
||||
const arrayHandle = await this._page.selectors._queryArray(this._context.frame, selector, this);
|
||||
const result = await arrayHandle._evaluateExpression(expression, isFunction, true, arg);
|
||||
arrayHandle.dispose();
|
||||
return result;
|
||||
|
|
@ -655,7 +655,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
|||
const { state = 'visible' } = options;
|
||||
if (!['attached', 'detached', 'visible', 'hidden'].includes(state))
|
||||
throw new Error(`state: expected one of (attached|detached|visible|hidden)`);
|
||||
const info = selectors._parseSelector(selector);
|
||||
const info = this._page.selectors._parseSelector(selector);
|
||||
const task = waitForSelectorTask(info, state, this);
|
||||
return this._page._runAbortableTask(async progress => {
|
||||
progress.log(`waiting for selector "${selector}"${state === 'attached' ? '' : ' to be ' + state}`);
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ import { FFExecutionContext } from './ffExecutionContext';
|
|||
import { RawKeyboardImpl, RawMouseImpl } from './ffInput';
|
||||
import { FFNetworkManager } from './ffNetworkManager';
|
||||
import { Protocol } from './protocol';
|
||||
import { selectors } from '../selectors';
|
||||
import { rewriteErrorMessage } from '../../utils/stackTrace';
|
||||
import { Screencast } from '../browserContext';
|
||||
|
||||
|
|
@ -489,7 +488,7 @@ export class FFPage implements PageDelegate {
|
|||
const parent = frame.parentFrame();
|
||||
if (!parent)
|
||||
throw new Error('Frame has been detached.');
|
||||
const handles = await selectors._queryAll(parent, 'iframe', undefined, true /* allowUtilityContext */);
|
||||
const handles = await this._page.selectors._queryAll(parent, 'iframe', undefined, true /* allowUtilityContext */);
|
||||
const items = await Promise.all(handles.map(async handle => {
|
||||
const frame = await handle.contentFrame().catch(e => null);
|
||||
return { handle, frame };
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import { helper, RegisteredListener } from './helper';
|
|||
import * as js from './javascript';
|
||||
import * as network from './network';
|
||||
import { Page } from './page';
|
||||
import { selectors } from './selectors';
|
||||
import * as types from './types';
|
||||
import { BrowserContext } from './browserContext';
|
||||
import { Progress, ProgressController } from './progress';
|
||||
|
|
@ -538,7 +537,7 @@ export class Frame extends EventEmitter {
|
|||
}
|
||||
|
||||
async $(selector: string): Promise<dom.ElementHandle<Element> | null> {
|
||||
return selectors._query(this, selector);
|
||||
return this._page.selectors._query(this, selector);
|
||||
}
|
||||
|
||||
async waitForSelector(selector: string, options: types.WaitForElementOptions = {}): Promise<dom.ElementHandle<Element> | null> {
|
||||
|
|
@ -549,7 +548,7 @@ export class Frame extends EventEmitter {
|
|||
const { state = 'visible' } = options;
|
||||
if (!['attached', 'detached', 'visible', 'hidden'].includes(state))
|
||||
throw new Error(`state: expected one of (attached|detached|visible|hidden)`);
|
||||
const info = selectors._parseSelector(selector);
|
||||
const info = this._page.selectors._parseSelector(selector);
|
||||
const task = dom.waitForSelectorTask(info, state);
|
||||
return this._page._runAbortableTask(async progress => {
|
||||
progress.log(`waiting for selector "${selector}"${state === 'attached' ? '' : ' to be ' + state}`);
|
||||
|
|
@ -564,7 +563,7 @@ export class Frame extends EventEmitter {
|
|||
}
|
||||
|
||||
async dispatchEvent(selector: string, type: string, eventInit?: Object, options: types.TimeoutOptions = {}): Promise<void> {
|
||||
const info = selectors._parseSelector(selector);
|
||||
const info = this._page.selectors._parseSelector(selector);
|
||||
const task = dom.dispatchEventTask(info, type, eventInit || {});
|
||||
await this._page._runAbortableTask(async progress => {
|
||||
progress.log(`Dispatching "${type}" event on selector "${selector}"...`);
|
||||
|
|
@ -584,14 +583,14 @@ export class Frame extends EventEmitter {
|
|||
}
|
||||
|
||||
async _$$evalExpression(selector: string, expression: string, isFunction: boolean, arg: any): Promise<any> {
|
||||
const arrayHandle = await selectors._queryArray(this, selector);
|
||||
const arrayHandle = await this._page.selectors._queryArray(this, selector);
|
||||
const result = await arrayHandle._evaluateExpression(expression, isFunction, true, arg);
|
||||
arrayHandle.dispose();
|
||||
return result;
|
||||
}
|
||||
|
||||
async $$(selector: string): Promise<dom.ElementHandle<Element>[]> {
|
||||
return selectors._queryAll(this, selector);
|
||||
return this._page.selectors._queryAll(this, selector);
|
||||
}
|
||||
|
||||
async content(): Promise<string> {
|
||||
|
|
@ -774,7 +773,7 @@ export class Frame extends EventEmitter {
|
|||
private async _retryWithSelectorIfNotConnected<R>(
|
||||
selector: string, options: types.TimeoutOptions,
|
||||
action: (progress: Progress, handle: dom.ElementHandle<Element>) => Promise<R | 'error:notconnected'>): Promise<R> {
|
||||
const info = selectors._parseSelector(selector);
|
||||
const info = this._page.selectors._parseSelector(selector);
|
||||
return this._page._runAbortableTask(async progress => {
|
||||
while (progress.isRunning()) {
|
||||
progress.log(`waiting for selector "${selector}"`);
|
||||
|
|
@ -816,7 +815,7 @@ export class Frame extends EventEmitter {
|
|||
}
|
||||
|
||||
async textContent(selector: string, options: types.TimeoutOptions = {}): Promise<string | null> {
|
||||
const info = selectors._parseSelector(selector);
|
||||
const info = this._page.selectors._parseSelector(selector);
|
||||
const task = dom.textContentTask(info);
|
||||
return this._page._runAbortableTask(async progress => {
|
||||
progress.log(` retrieving textContent from "${selector}"`);
|
||||
|
|
@ -825,7 +824,7 @@ export class Frame extends EventEmitter {
|
|||
}
|
||||
|
||||
async innerText(selector: string, options: types.TimeoutOptions = {}): Promise<string> {
|
||||
const info = selectors._parseSelector(selector);
|
||||
const info = this._page.selectors._parseSelector(selector);
|
||||
const task = dom.innerTextTask(info);
|
||||
return this._page._runAbortableTask(async progress => {
|
||||
progress.log(` retrieving innerText from "${selector}"`);
|
||||
|
|
@ -835,7 +834,7 @@ export class Frame extends EventEmitter {
|
|||
}
|
||||
|
||||
async innerHTML(selector: string, options: types.TimeoutOptions = {}): Promise<string> {
|
||||
const info = selectors._parseSelector(selector);
|
||||
const info = this._page.selectors._parseSelector(selector);
|
||||
const task = dom.innerHTMLTask(info);
|
||||
return this._page._runAbortableTask(async progress => {
|
||||
progress.log(` retrieving innerHTML from "${selector}"`);
|
||||
|
|
@ -844,7 +843,7 @@ export class Frame extends EventEmitter {
|
|||
}
|
||||
|
||||
async getAttribute(selector: string, name: string, options: types.TimeoutOptions = {}): Promise<string | null> {
|
||||
const info = selectors._parseSelector(selector);
|
||||
const info = this._page.selectors._parseSelector(selector);
|
||||
const task = dom.getAttributeTask(info, name);
|
||||
return this._page._runAbortableTask(async progress => {
|
||||
progress.log(` retrieving attribute "${name}" from "${selector}"`);
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import { FileChooser } from './fileChooser';
|
|||
import { Progress, runAbortableTask } from './progress';
|
||||
import { assert, isError } from '../utils/utils';
|
||||
import { debugLogger } from '../utils/debugLogger';
|
||||
import { Selectors } from './selectors';
|
||||
|
||||
export interface PageDelegate {
|
||||
readonly rawMouse: input.RawMouse;
|
||||
|
|
@ -138,6 +139,7 @@ export class Page extends EventEmitter {
|
|||
readonly coverage: any;
|
||||
private _requestInterceptor?: network.RouteHandler;
|
||||
_ownedContext: BrowserContext | undefined;
|
||||
readonly selectors: Selectors;
|
||||
|
||||
constructor(delegate: PageDelegate, browserContext: BrowserContext) {
|
||||
super();
|
||||
|
|
@ -164,6 +166,7 @@ export class Page extends EventEmitter {
|
|||
if (delegate.pdf)
|
||||
this.pdf = delegate.pdf.bind(delegate);
|
||||
this.coverage = delegate.coverage ? delegate.coverage() : null;
|
||||
this.selectors = browserContext.selectors();
|
||||
}
|
||||
|
||||
async _doSlowMo() {
|
||||
|
|
|
|||
|
|
@ -17,11 +17,11 @@
|
|||
import { Chromium } from './chromium/chromium';
|
||||
import { WebKit } from './webkit/webkit';
|
||||
import { Firefox } from './firefox/firefox';
|
||||
import { selectors } from './selectors';
|
||||
import * as browserPaths from '../utils/browserPaths';
|
||||
import { serverSelectors } from './selectors';
|
||||
|
||||
export class Playwright {
|
||||
readonly selectors = selectors;
|
||||
readonly selectors = serverSelectors;
|
||||
readonly chromium: Chromium;
|
||||
readonly firefox: Firefox;
|
||||
readonly webkit: WebKit;
|
||||
|
|
|
|||
|
|
@ -132,4 +132,4 @@ export class Selectors {
|
|||
}
|
||||
}
|
||||
|
||||
export const selectors = new Selectors();
|
||||
export const serverSelectors = new Selectors();
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ import * as accessibility from '../accessibility';
|
|||
import { getAccessibilityTree } from './wkAccessibility';
|
||||
import { WKProvisionalPage } from './wkProvisionalPage';
|
||||
import { WKBrowserContext } from './wkBrowser';
|
||||
import { selectors } from '../selectors';
|
||||
import * as jpeg from 'jpeg-js';
|
||||
import * as png from 'pngjs';
|
||||
import { JSHandle } from '../javascript';
|
||||
|
|
@ -853,7 +852,7 @@ export class WKPage implements PageDelegate {
|
|||
const parent = frame.parentFrame();
|
||||
if (!parent)
|
||||
throw new Error('Frame has been detached.');
|
||||
const handles = await selectors._queryAll(parent, 'iframe', undefined, true /* allowUtilityContext */);
|
||||
const handles = await this._page.selectors._queryAll(parent, 'iframe', undefined, true /* allowUtilityContext */);
|
||||
const items = await Promise.all(handles.map(async handle => {
|
||||
const frame = await handle.contentFrame().catch(e => null);
|
||||
return { handle, frame };
|
||||
|
|
|
|||
|
|
@ -163,9 +163,7 @@ describe('connect', suite => {
|
|||
}
|
||||
});
|
||||
|
||||
it('should respect selectors', test => {
|
||||
test.fail(true);
|
||||
}, async ({ playwright, browserType, remoteServer }) => {
|
||||
it('should respect selectors', async ({ playwright, browserType, remoteServer }) => {
|
||||
const mycss = () => ({
|
||||
create(root, target) {},
|
||||
query(root, selector) {
|
||||
|
|
@ -175,13 +173,33 @@ describe('connect', suite => {
|
|||
return Array.from(root.querySelectorAll(selector));
|
||||
}
|
||||
});
|
||||
await utils.registerEngine(playwright, 'mycss', mycss);
|
||||
// Register one engine before connecting.
|
||||
await utils.registerEngine(playwright, 'mycss1', mycss);
|
||||
|
||||
const browser = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() });
|
||||
const page = await browser.newPage();
|
||||
await page.setContent(`<div>hello</div>`);
|
||||
expect(await page.innerHTML('css=div')).toBe('hello');
|
||||
expect(await page.innerHTML('mycss=div')).toBe('hello');
|
||||
await browser.close();
|
||||
const browser1 = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() });
|
||||
const context1 = await browser1.newContext();
|
||||
|
||||
// Register another engine after creating context.
|
||||
await utils.registerEngine(playwright, 'mycss2', mycss);
|
||||
|
||||
const page1 = await context1.newPage();
|
||||
await page1.setContent(`<div>hello</div>`);
|
||||
expect(await page1.innerHTML('css=div')).toBe('hello');
|
||||
expect(await page1.innerHTML('mycss1=div')).toBe('hello');
|
||||
expect(await page1.innerHTML('mycss2=div')).toBe('hello');
|
||||
|
||||
const browser2 = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() });
|
||||
|
||||
// Register third engine after second connect.
|
||||
await utils.registerEngine(playwright, 'mycss3', mycss);
|
||||
|
||||
const page2 = await browser2.newPage();
|
||||
await page2.setContent(`<div>hello</div>`);
|
||||
expect(await page2.innerHTML('css=div')).toBe('hello');
|
||||
expect(await page2.innerHTML('mycss1=div')).toBe('hello');
|
||||
expect(await page2.innerHTML('mycss2=div')).toBe('hello');
|
||||
expect(await page2.innerHTML('mycss3=div')).toBe('hello');
|
||||
|
||||
await browser1.close();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -227,3 +227,22 @@ it('coverage should be missing', test => {
|
|||
const {page} = await launchPersistent();
|
||||
expect(page.coverage).toBe(null);
|
||||
});
|
||||
|
||||
it('should respect selectors', async ({playwright, launchPersistent}) => {
|
||||
const {page} = await launchPersistent();
|
||||
|
||||
const defaultContextCSS = () => ({
|
||||
create(root, target) {},
|
||||
query(root, selector) {
|
||||
return root.querySelector(selector);
|
||||
},
|
||||
queryAll(root: HTMLElement, selector: string) {
|
||||
return Array.from(root.querySelectorAll(selector));
|
||||
}
|
||||
});
|
||||
await utils.registerEngine(playwright, 'defaultContextCSS', defaultContextCSS);
|
||||
|
||||
await page.setContent(`<div>hello</div>`);
|
||||
expect(await page.innerHTML('css=div')).toBe('hello');
|
||||
expect(await page.innerHTML('defaultContextCSS=div')).toBe('hello');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import './playwright.fixtures';
|
|||
import path from 'path';
|
||||
import utils from './utils';
|
||||
|
||||
it('should work', async ({playwright, page}) => {
|
||||
it('should work', async ({playwright, browser}) => {
|
||||
const createTagSelector = () => ({
|
||||
create(root, target) {
|
||||
return target.nodeName;
|
||||
|
|
@ -32,16 +32,31 @@ it('should work', async ({playwright, page}) => {
|
|||
return Array.from(root.querySelectorAll(selector));
|
||||
}
|
||||
});
|
||||
// Register one engine before creating context.
|
||||
await utils.registerEngine(playwright, 'tag', `(${createTagSelector.toString()})()`);
|
||||
|
||||
const context = await browser.newContext();
|
||||
// Register another engine after creating context.
|
||||
await utils.registerEngine(playwright, 'tag2', `(${createTagSelector.toString()})()`);
|
||||
|
||||
const page = await context.newPage();
|
||||
await page.setContent('<div><span></span></div><div></div>');
|
||||
expect(await (playwright.selectors as any)._createSelector('tag', await page.$('div'))).toBe('DIV');
|
||||
|
||||
expect(await (await page.$('div') as any)._createSelectorForTest('tag')).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);
|
||||
|
||||
expect(await (await page.$('div') as any)._createSelectorForTest('tag2')).toBe('DIV');
|
||||
expect(await page.$eval('tag2=DIV', e => e.nodeName)).toBe('DIV');
|
||||
expect(await page.$eval('tag2=SPAN', e => e.nodeName)).toBe('SPAN');
|
||||
expect(await page.$$eval('tag2=DIV', es => es.length)).toBe(2);
|
||||
|
||||
// Selector names are case-sensitive.
|
||||
const error = await page.$('tAG=DIV').catch(e => e);
|
||||
expect(error.message).toContain('Unknown engine "tAG" while parsing selector tAG=DIV');
|
||||
|
||||
await context.close();
|
||||
});
|
||||
|
||||
it('should work with path', async ({playwright, page}) => {
|
||||
|
|
|
|||
|
|
@ -96,20 +96,20 @@ it('query', async ({page}) => {
|
|||
expect(await page.$$eval(`text=lowo`, els => els.map(e => e.outerHTML).join(''))).toBe('<div>helloworld</div><span>helloworld</span>');
|
||||
});
|
||||
|
||||
it('create', async ({playwright, page}) => {
|
||||
it('create', async ({page}) => {
|
||||
await page.setContent(`<div>yo</div><div>"ya</div><div>ye ye</div>`);
|
||||
expect(await (playwright.selectors as any)._createSelector('text', await page.$('div'))).toBe('yo');
|
||||
expect(await (playwright.selectors as any)._createSelector('text', await page.$('div:nth-child(2)'))).toBe('"\\"ya"');
|
||||
expect(await (playwright.selectors as any)._createSelector('text', await page.$('div:nth-child(3)'))).toBe('"ye ye"');
|
||||
expect(await (await page.$('div') as any)._createSelectorForTest('text')).toBe('yo');
|
||||
expect(await (await page.$('div:nth-child(2)') as any)._createSelectorForTest('text')).toBe('"\\"ya"');
|
||||
expect(await (await page.$('div:nth-child(3)') as any)._createSelectorForTest('text')).toBe('"ye ye"');
|
||||
|
||||
await page.setContent(`<div>yo</div><div>yo<div>ya</div>hey</div>`);
|
||||
expect(await (playwright.selectors as any)._createSelector('text', await page.$('div:nth-child(2)'))).toBe('hey');
|
||||
expect(await (await page.$('div:nth-child(2)') as any)._createSelectorForTest('text')).toBe('hey');
|
||||
|
||||
await page.setContent(`<div> yo <div></div>ya</div>`);
|
||||
expect(await (playwright.selectors as any)._createSelector('text', await page.$('div'))).toBe('yo');
|
||||
expect(await (await page.$('div') as any)._createSelectorForTest('text')).toBe('yo');
|
||||
|
||||
await page.setContent(`<div> "yo <div></div>ya</div>`);
|
||||
expect(await (playwright.selectors as any)._createSelector('text', await page.$('div'))).toBe('" \\"yo "');
|
||||
expect(await (await page.$('div') as any)._createSelectorForTest('text')).toBe('" \\"yo "');
|
||||
});
|
||||
|
||||
it('should be case sensitive if quotes are specified', async ({page}) => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue