diff --git a/src/browserServerImpl.ts b/src/browserServerImpl.ts index 97b513d95f8d8..34d0f4365fc88 100644 --- a/src/browserServerImpl.ts +++ b/src/browserServerImpl.ts @@ -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; } diff --git a/src/client/browserType.ts b/src/client/browserType.ts index cab3b50f4446a..d0c3158896ec6 100644 --- a/src/client/browserType.ts +++ b/src/client/browserType.ts @@ -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; @@ -144,7 +145,13 @@ export class BrowserType extends ChannelOwner { - 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 { + sharedSelectors._removeChannel(selectorsOwner); ws.removeEventListener('close', closeListener); ws.close(); }); @@ -171,3 +179,6 @@ export class BrowserType extends ChannelOwner { +} diff --git a/src/client/connection.ts b/src/client/connection.ts index dd001c2d18cf1..53557b81c1508 100644 --- a/src/client/connection.ts +++ b/src/client/connection.ts @@ -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 { 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); diff --git a/src/client/elementHandle.ts b/src/client/elementHandle.ts index cf0971b636ce2..e0e7060455dcc 100644 --- a/src/client/elementHandle.ts +++ b/src/client/elementHandle.ts @@ -233,6 +233,10 @@ export class ElementHandle extends JSHandle { return ElementHandle.fromNullable(result.element) as ElementHandle | null; }); } + + async _createSelectorForTest(name: string): Promise { + return (await this._elementChannel.createSelectorForTest({ name })).value; + } } export function convertSelectOptionValues(values: string | ElementHandle | SelectOption | string[] | ElementHandle[] | SelectOption[] | null): { elements?: channels.ElementHandleChannel[], options?: SelectOption[] } { diff --git a/src/client/playwright.ts b/src/client/playwright.ts index 159256ed80bc9..a24eaf474b567 100644 --- a/src/client/playwright.ts +++ b/src/client/playwright.ts @@ -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 { - static from(selectors: channels.SelectorsChannel): Selectors { - return (selectors as any)._object; +export class Selectors { + private _channels = new Set(); + private _registrations: channels.SelectorsRegisterParams[] = []; + + async register(name: string, script: string | Function | { path?: string, content?: string }, options: { contentScript?: boolean } = {}): Promise { + const source = await evaluationScript(script, undefined, false); + const params = { ...options, name, source }; + for (const channel of this._channels) + await channel._channel.register(params); + this._registrations.push(params); } - constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.SelectorsInitializer) { - super(parent, type, guid, initializer); + _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 => {}); + } } - async register(name: string, script: string | Function | { path?: string, content?: string }, options: { contentScript?: boolean } = {}): Promise { - const source = await evaluationScript(script, undefined, false); - await this._channel.register({ ...options, name, source }); + _removeChannel(channel: SelectorsOwner) { + this._channels.delete(channel); } +} - async _createSelector(name: string, handle: ElementHandle): Promise { - return (await this._channel.createSelector({ name, handle: handle._elementChannel })).value; +export class SelectorsOwner extends ChannelOwner { + static from(browser: channels.SelectorsChannel): SelectorsOwner { + return (browser as any)._object; } } + +export const sharedSelectors = new Selectors(); diff --git a/src/dispatchers/browserDispatcher.ts b/src/dispatchers/browserDispatcher.ts index edde7e05b7eb0..f035ea9655a95 100644 --- a/src/dispatchers/browserDispatcher.ts +++ b/src/dispatchers/browserDispatcher.ts @@ -23,8 +23,8 @@ import { CRBrowser } from '../server/chromium/crBrowser'; import { PageDispatcher } from './pageDispatcher'; export class BrowserDispatcher extends Dispatcher 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()); } diff --git a/src/dispatchers/elementHandlerDispatcher.ts b/src/dispatchers/elementHandlerDispatcher.ts index 74f7a008c3770..34612af1be41a 100644 --- a/src/dispatchers/elementHandlerDispatcher.ts +++ b/src/dispatchers/elementHandlerDispatcher.ts @@ -156,4 +156,8 @@ export class ElementHandleDispatcher extends JSHandleDispatcher implements chann async waitForSelector(params: channels.ElementHandleWaitForSelectorParams): Promise { return { element: ElementHandleDispatcher.createNullable(this._scope, await this._elementHandle.waitForSelector(params.selector, params)) }; } + + async createSelectorForTest(params: channels.ElementHandleCreateSelectorForTestParams): Promise { + return { value: await this._elementHandle._page.selectors._createSelector(params.name, this._elementHandle as ElementHandle) }; + } } diff --git a/src/dispatchers/playwrightDispatcher.ts b/src/dispatchers/playwrightDispatcher.ts index f4b0a56aba6f1..1ae90c063d0d5 100644 --- a/src/dispatchers/playwrightDispatcher.ts +++ b/src/dispatchers/playwrightDispatcher.ts @@ -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 implements channels.PlaywrightChannel { constructor(scope: DispatcherScope, playwright: Playwright) { diff --git a/src/dispatchers/selectorsDispatcher.ts b/src/dispatchers/selectorsDispatcher.ts index 53e2ad6a44148..894f85b64f3fc 100644 --- a/src/dispatchers/selectorsDispatcher.ts +++ b/src/dispatchers/selectorsDispatcher.ts @@ -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 implements channels.SelectorsChannel { constructor(scope: DispatcherScope, selectors: Selectors) { @@ -28,8 +26,4 @@ export class SelectorsDispatcher extends Dispatcher { await this._object.register(params.name, params.source, params.contentScript); } - - async createSelector(params: channels.SelectorsCreateSelectorParams): Promise { - return { value: await this._object._createSelector(params.name, (params.handle as ElementHandleDispatcher)._object as dom.ElementHandle) }; - } } diff --git a/src/protocol/channels.ts b/src/protocol/channels.ts index 746a98383335d..b1d81488e62b6 100644 --- a/src/protocol/channels.ts +++ b/src/protocol/channels.ts @@ -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; - createSelector(params: SelectorsCreateSelectorParams): Promise; } 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; waitForElementState(params: ElementHandleWaitForElementStateParams): Promise; waitForSelector(params: ElementHandleWaitForSelectorParams): Promise; + createSelectorForTest(params: ElementHandleCreateSelectorForTestParams): Promise; } 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 = { diff --git a/src/protocol/protocol.yml b/src/protocol/protocol.yml index 2639d3975bdf2..ae22f95941a73 100644 --- a/src/protocol/protocol.yml +++ b/src/protocol/protocol.yml @@ -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 diff --git a/src/protocol/validator.ts b/src/protocol/validator.ts index f84aa74d8e635..9d6d4f5e4610f 100644 --- a/src/protocol/validator.ts +++ b/src/protocol/validator.ts @@ -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), diff --git a/src/server/browserContext.ts b/src/server/browserContext.ts index 90a6b125b545b..252d51deceaea 100644 --- a/src/server/browserContext.ts +++ b/src/server/browserContext.ts @@ -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(); 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); diff --git a/src/server/dom.ts b/src/server/dom.ts index b9d1b5e799a52..6134028a7a696 100644 --- a/src/server/dom.ts +++ b/src/server/dom.ts @@ -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> { 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 extends js.JSHandle { } async $(selector: string): Promise { - return selectors._query(this._context.frame, selector, this); + return this._page.selectors._query(this._context.frame, selector, this); } async $$(selector: string): Promise[]> { - 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 { - 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 extends js.JSHandle { } async _$$evalExpression(selector: string, expression: string, isFunction: boolean, arg: any): Promise { - 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 extends js.JSHandle { 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}`); diff --git a/src/server/firefox/ffPage.ts b/src/server/firefox/ffPage.ts index 2f3bf24998903..bea2af02845c8 100644 --- a/src/server/firefox/ffPage.ts +++ b/src/server/firefox/ffPage.ts @@ -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 }; diff --git a/src/server/frames.ts b/src/server/frames.ts index ff19ce3c0123e..636e42dcc73ac 100644 --- a/src/server/frames.ts +++ b/src/server/frames.ts @@ -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 | null> { - return selectors._query(this, selector); + return this._page.selectors._query(this, selector); } async waitForSelector(selector: string, options: types.WaitForElementOptions = {}): Promise | 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 { - 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 { - 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[]> { - return selectors._queryAll(this, selector); + return this._page.selectors._queryAll(this, selector); } async content(): Promise { @@ -774,7 +773,7 @@ export class Frame extends EventEmitter { private async _retryWithSelectorIfNotConnected( selector: string, options: types.TimeoutOptions, action: (progress: Progress, handle: dom.ElementHandle) => Promise): Promise { - 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 { - 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 { - 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 { - 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 { - 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}"`); diff --git a/src/server/page.ts b/src/server/page.ts index ba89b5fe77641..6a2f491a026b0 100644 --- a/src/server/page.ts +++ b/src/server/page.ts @@ -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() { diff --git a/src/server/playwright.ts b/src/server/playwright.ts index 295d9981c609c..9ecadb0ad73cd 100644 --- a/src/server/playwright.ts +++ b/src/server/playwright.ts @@ -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; diff --git a/src/server/selectors.ts b/src/server/selectors.ts index dcc3820919046..b7cfa05c37fa9 100644 --- a/src/server/selectors.ts +++ b/src/server/selectors.ts @@ -132,4 +132,4 @@ export class Selectors { } } -export const selectors = new Selectors(); +export const serverSelectors = new Selectors(); diff --git a/src/server/webkit/wkPage.ts b/src/server/webkit/wkPage.ts index 37a3a7d0f72f7..1e1f296903cba 100644 --- a/src/server/webkit/wkPage.ts +++ b/src/server/webkit/wkPage.ts @@ -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 }; diff --git a/test/browsertype-connect.spec.ts b/test/browsertype-connect.spec.ts index 3845baa639357..5e89d2c4b5c0c 100644 --- a/test/browsertype-connect.spec.ts +++ b/test/browsertype-connect.spec.ts @@ -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); - - const browser = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); - const page = await browser.newPage(); - await page.setContent(`
hello
`); - expect(await page.innerHTML('css=div')).toBe('hello'); - expect(await page.innerHTML('mycss=div')).toBe('hello'); - await browser.close(); + // Register one engine before connecting. + await utils.registerEngine(playwright, 'mycss1', mycss); + + 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(`
hello
`); + 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(`
hello
`); + 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(); }); }); diff --git a/test/defaultbrowsercontext-2.spec.ts b/test/defaultbrowsercontext-2.spec.ts index b41056b0f4f97..93f36233bef9e 100644 --- a/test/defaultbrowsercontext-2.spec.ts +++ b/test/defaultbrowsercontext-2.spec.ts @@ -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(`
hello
`); + expect(await page.innerHTML('css=div')).toBe('hello'); + expect(await page.innerHTML('defaultContextCSS=div')).toBe('hello'); +}); diff --git a/test/selectors-register.spec.ts b/test/selectors-register.spec.ts index e12dc27ca1680..7ab767f7fb64e 100644 --- a/test/selectors-register.spec.ts +++ b/test/selectors-register.spec.ts @@ -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('
'); - 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}) => { diff --git a/test/selectors-text.spec.ts b/test/selectors-text.spec.ts index d379c536d07a7..ade4fd6b42828 100644 --- a/test/selectors-text.spec.ts +++ b/test/selectors-text.spec.ts @@ -96,20 +96,20 @@ it('query', async ({page}) => { expect(await page.$$eval(`text=lowo`, els => els.map(e => e.outerHTML).join(''))).toBe('
helloworld
helloworld'); }); -it('create', async ({playwright, page}) => { +it('create', async ({page}) => { await page.setContent(`
yo
"ya
ye ye
`); - 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(`
yo
yo
ya
hey
`); - 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(`
yo
ya
`); - 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(`
"yo
ya
`); - 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}) => {