chore: identify largest gaps in Bidi API (#32434)
This pull request introduces initial support for the WebDriver BiDi protocol in Playwright. The primary goal of this PR is not to fully implement BiDi but to experiment with the current state of the specification and its implementation. We aim to identify the biggest gaps and challenges that need to be addressed before considering BiDi as the main protocol for Playwright.
This commit is contained in:
parent
a87426ee0d
commit
9a2c60a77c
|
|
@ -24,6 +24,7 @@
|
||||||
"webview2test": "playwright test --config=tests/webview2/playwright.config.ts",
|
"webview2test": "playwright test --config=tests/webview2/playwright.config.ts",
|
||||||
"itest": "playwright test --config=tests/installation/playwright.config.ts",
|
"itest": "playwright test --config=tests/installation/playwright.config.ts",
|
||||||
"stest": "playwright test --config=tests/stress/playwright.config.ts",
|
"stest": "playwright test --config=tests/stress/playwright.config.ts",
|
||||||
|
"biditest": "playwright test --config=tests/bidi/playwright.config.ts",
|
||||||
"test-html-reporter": "playwright test --config=packages/html-reporter",
|
"test-html-reporter": "playwright test --config=packages/html-reporter",
|
||||||
"test-web": "playwright test --config=packages/web",
|
"test-web": "playwright test --config=packages/web",
|
||||||
"ttest": "node ./tests/playwright-test/stable-test-runner/node_modules/@playwright/test/cli test --config=tests/playwright-test/playwright.config.ts",
|
"ttest": "node ./tests/playwright-test/stable-test-runner/node_modules/@playwright/test/cli test --config=tests/playwright-test/playwright.config.ts",
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import { Selectors, SelectorsOwner } from './selectors';
|
||||||
export class Playwright extends ChannelOwner<channels.PlaywrightChannel> {
|
export class Playwright extends ChannelOwner<channels.PlaywrightChannel> {
|
||||||
readonly _android: Android;
|
readonly _android: Android;
|
||||||
readonly _electron: Electron;
|
readonly _electron: Electron;
|
||||||
|
readonly _experimentalBidi: BrowserType;
|
||||||
readonly chromium: BrowserType;
|
readonly chromium: BrowserType;
|
||||||
readonly firefox: BrowserType;
|
readonly firefox: BrowserType;
|
||||||
readonly webkit: BrowserType;
|
readonly webkit: BrowserType;
|
||||||
|
|
@ -45,6 +46,8 @@ export class Playwright extends ChannelOwner<channels.PlaywrightChannel> {
|
||||||
this.webkit._playwright = this;
|
this.webkit._playwright = this;
|
||||||
this._android = Android.from(initializer.android);
|
this._android = Android.from(initializer.android);
|
||||||
this._electron = Electron.from(initializer.electron);
|
this._electron = Electron.from(initializer.electron);
|
||||||
|
this._experimentalBidi = BrowserType.from(initializer.bidi);
|
||||||
|
this._experimentalBidi._playwright = this;
|
||||||
this.devices = this._connection.localUtils()?.devices ?? {};
|
this.devices = this._connection.localUtils()?.devices ?? {};
|
||||||
this.selectors = new Selectors();
|
this.selectors = new Selectors();
|
||||||
this.errors = { TimeoutError };
|
this.errors = { TimeoutError };
|
||||||
|
|
|
||||||
|
|
@ -321,6 +321,7 @@ scheme.RootInitializeResult = tObject({
|
||||||
});
|
});
|
||||||
scheme.PlaywrightInitializer = tObject({
|
scheme.PlaywrightInitializer = tObject({
|
||||||
chromium: tChannel(['BrowserType']),
|
chromium: tChannel(['BrowserType']),
|
||||||
|
bidi: tChannel(['BrowserType']),
|
||||||
firefox: tChannel(['BrowserType']),
|
firefox: tChannel(['BrowserType']),
|
||||||
webkit: tChannel(['BrowserType']),
|
webkit: tChannel(['BrowserType']),
|
||||||
android: tChannel(['Android']),
|
android: tChannel(['Android']),
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
[playwright.ts]
|
[playwright.ts]
|
||||||
./android/
|
./android/
|
||||||
|
./bidi/
|
||||||
./chromium/
|
./chromium/
|
||||||
./electron/
|
./electron/
|
||||||
./firefox/
|
./firefox/
|
||||||
|
|
|
||||||
5
packages/playwright-core/src/server/bidi/DEPS.list
Normal file
5
packages/playwright-core/src/server/bidi/DEPS.list
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
[*]
|
||||||
|
../../utils/
|
||||||
|
../
|
||||||
|
../isomorphic/
|
||||||
|
./third_party/
|
||||||
332
packages/playwright-core/src/server/bidi/bidiBrowser.ts
Normal file
332
packages/playwright-core/src/server/bidi/bidiBrowser.ts
Normal file
|
|
@ -0,0 +1,332 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the 'License');
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an 'AS IS' BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type * as channels from '@protocol/channels';
|
||||||
|
import type { RegisteredListener } from '../../utils/eventsHelper';
|
||||||
|
import { eventsHelper } from '../../utils/eventsHelper';
|
||||||
|
import type { BrowserOptions } from '../browser';
|
||||||
|
import { Browser } from '../browser';
|
||||||
|
import { assertBrowserContextIsNotOwned, BrowserContext } from '../browserContext';
|
||||||
|
import type { SdkObject } from '../instrumentation';
|
||||||
|
import * as network from '../network';
|
||||||
|
import type { InitScript, Page, PageDelegate } from '../page';
|
||||||
|
import type { ConnectionTransport } from '../transport';
|
||||||
|
import type * as types from '../types';
|
||||||
|
import type { BidiSession } from './bidiConnection';
|
||||||
|
import { BidiConnection } from './bidiConnection';
|
||||||
|
import { bidiBytesValueToString } from './bidiNetworkManager';
|
||||||
|
import { BidiPage } from './bidiPage';
|
||||||
|
import * as bidi from './third_party/bidiProtocol';
|
||||||
|
|
||||||
|
export class BidiBrowser extends Browser {
|
||||||
|
private readonly _connection: BidiConnection;
|
||||||
|
readonly _browserSession: BidiSession;
|
||||||
|
private _bidiSessionInfo!: bidi.Session.NewResult;
|
||||||
|
readonly _contexts = new Map<string, BidiBrowserContext>();
|
||||||
|
readonly _bidiPages = new Map<bidi.BrowsingContext.BrowsingContext, BidiPage>();
|
||||||
|
private readonly _eventListeners: RegisteredListener[];
|
||||||
|
|
||||||
|
static async connect(parent: SdkObject, transport: ConnectionTransport, options: BrowserOptions): Promise<BidiBrowser> {
|
||||||
|
const browser = new BidiBrowser(parent, transport, options);
|
||||||
|
if ((options as any).__testHookOnConnectToBrowser)
|
||||||
|
await (options as any).__testHookOnConnectToBrowser();
|
||||||
|
const sessionStatus = await browser._browserSession.send('session.status', {});
|
||||||
|
if (!sessionStatus.ready)
|
||||||
|
throw new Error('Bidi session is not ready. ' + sessionStatus.message);
|
||||||
|
|
||||||
|
let proxy: bidi.Session.ManualProxyConfiguration | undefined;
|
||||||
|
if (options.proxy) {
|
||||||
|
proxy = {
|
||||||
|
proxyType: 'manual',
|
||||||
|
};
|
||||||
|
const url = new URL(options.proxy.server); // Validate proxy server.
|
||||||
|
switch (url.protocol) {
|
||||||
|
case 'http:':
|
||||||
|
proxy.httpProxy = url.host;
|
||||||
|
break;
|
||||||
|
case 'https:':
|
||||||
|
proxy.httpsProxy = url.host;
|
||||||
|
break;
|
||||||
|
case 'socks4:':
|
||||||
|
proxy.socksProxy = url.host;
|
||||||
|
proxy.socksVersion = 4;
|
||||||
|
break;
|
||||||
|
case 'socks5:':
|
||||||
|
proxy.socksProxy = url.host;
|
||||||
|
proxy.socksVersion = 5;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error('Invalid proxy server protocol: ' + options.proxy.server);
|
||||||
|
}
|
||||||
|
if (options.proxy.bypass)
|
||||||
|
proxy.noProxy = options.proxy.bypass.split(',');
|
||||||
|
// TODO: support authentication.
|
||||||
|
}
|
||||||
|
|
||||||
|
browser._bidiSessionInfo = await browser._browserSession.send('session.new', {
|
||||||
|
capabilities: {
|
||||||
|
alwaysMatch: {
|
||||||
|
acceptInsecureCerts: false,
|
||||||
|
proxy,
|
||||||
|
unhandledPromptBehavior: {
|
||||||
|
default: bidi.Session.UserPromptHandlerType.Ignore,
|
||||||
|
},
|
||||||
|
webSocketUrl: true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await browser._browserSession.send('session.subscribe', {
|
||||||
|
events: [
|
||||||
|
'browsingContext',
|
||||||
|
'network',
|
||||||
|
'log',
|
||||||
|
'script',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return browser;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(parent: SdkObject, transport: ConnectionTransport, options: BrowserOptions) {
|
||||||
|
super(parent, options);
|
||||||
|
this._connection = new BidiConnection(transport, this._onDisconnect.bind(this), options.protocolLogger, options.browserLogsCollector);
|
||||||
|
this._browserSession = this._connection.browserSession;
|
||||||
|
this._eventListeners = [
|
||||||
|
eventsHelper.addEventListener(this._browserSession, 'browsingContext.contextCreated', this._onBrowsingContextCreated.bind(this)),
|
||||||
|
eventsHelper.addEventListener(this._browserSession, 'script.realmDestroyed', this._onScriptRealmDestroyed.bind(this)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
_onDisconnect() {
|
||||||
|
this._didClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
async doCreateNewContext(options: channels.BrowserNewContextParams): Promise<BrowserContext> {
|
||||||
|
const { userContext } = await this._browserSession.send('browser.createUserContext', {});
|
||||||
|
const context = new BidiBrowserContext(this, userContext, options);
|
||||||
|
await context._initialize();
|
||||||
|
this._contexts.set(userContext, context);
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
contexts(): BrowserContext[] {
|
||||||
|
return Array.from(this._contexts.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
version(): string {
|
||||||
|
return this._bidiSessionInfo.capabilities.browserVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
userAgent(): string {
|
||||||
|
return this._bidiSessionInfo.capabilities.userAgent;
|
||||||
|
}
|
||||||
|
|
||||||
|
isConnected(): boolean {
|
||||||
|
return !this._connection.isClosed();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onBrowsingContextCreated(event: bidi.BrowsingContext.Info) {
|
||||||
|
if (event.parent) {
|
||||||
|
const parentFrameId = event.parent;
|
||||||
|
for (const page of this._bidiPages.values()) {
|
||||||
|
const parentFrame = page._page._frameManager.frame(parentFrameId);
|
||||||
|
if (!parentFrame)
|
||||||
|
continue;
|
||||||
|
page._session.addFrameBrowsingContext(event.context);
|
||||||
|
page._page._frameManager.frameAttached(event.context, parentFrameId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let context = this._contexts.get(event.userContext);
|
||||||
|
if (!context)
|
||||||
|
context = this._defaultContext as BidiBrowserContext;
|
||||||
|
if (!context)
|
||||||
|
return;
|
||||||
|
const session = this._connection.createMainFrameBrowsingContextSession(event.context);
|
||||||
|
const opener = event.originalOpener && this._bidiPages.get(event.originalOpener);
|
||||||
|
const page = new BidiPage(context, session, opener || null);
|
||||||
|
this._bidiPages.set(event.context, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onBrowsingContextDestroyed(event: bidi.BrowsingContext.Info) {
|
||||||
|
if (event.parent) {
|
||||||
|
this._browserSession.removeFrameBrowsingContext(event.context);
|
||||||
|
const parentFrameId = event.parent;
|
||||||
|
for (const page of this._bidiPages.values()) {
|
||||||
|
const parentFrame = page._page._frameManager.frame(parentFrameId);
|
||||||
|
if (!parentFrame)
|
||||||
|
continue;
|
||||||
|
page._page._frameManager.frameDetached(event.context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const bidiPage = this._bidiPages.get(event.context);
|
||||||
|
if (!bidiPage)
|
||||||
|
return;
|
||||||
|
bidiPage.didClose();
|
||||||
|
this._bidiPages.delete(event.context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onScriptRealmDestroyed(event: bidi.Script.RealmDestroyedParameters) {
|
||||||
|
for (const page of this._bidiPages.values()) {
|
||||||
|
if (page._onRealmDestroyed(event))
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BidiBrowserContext extends BrowserContext {
|
||||||
|
declare readonly _browser: BidiBrowser;
|
||||||
|
|
||||||
|
constructor(browser: BidiBrowser, browserContextId: string | undefined, options: channels.BrowserNewContextParams) {
|
||||||
|
super(browser, options, browserContextId);
|
||||||
|
this._authenticateProxyViaHeader();
|
||||||
|
}
|
||||||
|
|
||||||
|
pages(): Page[] {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async newPageDelegate(): Promise<PageDelegate> {
|
||||||
|
assertBrowserContextIsNotOwned(this);
|
||||||
|
const { context } = await this._browser._browserSession.send('browsingContext.create', {
|
||||||
|
type: bidi.BrowsingContext.CreateType.Window,
|
||||||
|
userContext: this._browserContextId,
|
||||||
|
});
|
||||||
|
return this._browser._bidiPages.get(context)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
async doGetCookies(urls: string[]): Promise<channels.NetworkCookie[]> {
|
||||||
|
const { cookies } = await this._browser._browserSession.send('storage.getCookies',
|
||||||
|
{ partition: { type: 'storageKey', userContext: this._browserContextId } });
|
||||||
|
return network.filterCookies(cookies.map((c: bidi.Network.Cookie) => {
|
||||||
|
const copy: channels.NetworkCookie = {
|
||||||
|
name: c.name,
|
||||||
|
value: bidiBytesValueToString(c.value),
|
||||||
|
domain: c.domain,
|
||||||
|
path: c.path,
|
||||||
|
httpOnly: c.httpOnly,
|
||||||
|
secure: c.secure,
|
||||||
|
expires: c.expiry ?? -1,
|
||||||
|
sameSite: c.sameSite ? fromBidiSameSite(c.sameSite) : 'None',
|
||||||
|
};
|
||||||
|
return copy;
|
||||||
|
}), urls);
|
||||||
|
}
|
||||||
|
|
||||||
|
async addCookies(cookies: channels.SetNetworkCookie[]) {
|
||||||
|
cookies = network.rewriteCookies(cookies);
|
||||||
|
const promises = cookies.map((c: channels.SetNetworkCookie) => {
|
||||||
|
const cookie: bidi.Storage.PartialCookie = {
|
||||||
|
name: c.name,
|
||||||
|
value: { type: 'string', value: c.value },
|
||||||
|
domain: c.domain!,
|
||||||
|
path: c.path,
|
||||||
|
httpOnly: c.httpOnly,
|
||||||
|
secure: c.secure,
|
||||||
|
sameSite: c.sameSite && toBidiSameSite(c.sameSite),
|
||||||
|
expiry: (c.expires === -1 || c.expires === undefined) ? undefined : Math.round(c.expires),
|
||||||
|
};
|
||||||
|
return this._browser._browserSession.send('storage.setCookie',
|
||||||
|
{ cookie, partition: { type: 'storageKey', userContext: this._browserContextId } });
|
||||||
|
});
|
||||||
|
await Promise.all(promises);
|
||||||
|
}
|
||||||
|
|
||||||
|
async doClearCookies() {
|
||||||
|
await this._browser._browserSession.send('storage.deleteCookies',
|
||||||
|
{ partition: { type: 'storageKey', userContext: this._browserContextId } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async doGrantPermissions(origin: string, permissions: string[]) {
|
||||||
|
}
|
||||||
|
|
||||||
|
async doClearPermissions() {
|
||||||
|
}
|
||||||
|
|
||||||
|
async setGeolocation(geolocation?: types.Geolocation): Promise<void> {
|
||||||
|
}
|
||||||
|
|
||||||
|
async setExtraHTTPHeaders(headers: types.HeadersArray): Promise<void> {
|
||||||
|
}
|
||||||
|
|
||||||
|
async setUserAgent(userAgent: string | undefined): Promise<void> {
|
||||||
|
}
|
||||||
|
|
||||||
|
async setOffline(offline: boolean): Promise<void> {
|
||||||
|
}
|
||||||
|
|
||||||
|
async doSetHTTPCredentials(httpCredentials?: types.Credentials): Promise<void> {
|
||||||
|
}
|
||||||
|
|
||||||
|
async doAddInitScript(initScript: InitScript) {
|
||||||
|
// for (const page of this.pages())
|
||||||
|
// await (page._delegate as WKPage)._updateBootstrapScript();
|
||||||
|
}
|
||||||
|
|
||||||
|
async doRemoveNonInternalInitScripts() {
|
||||||
|
}
|
||||||
|
|
||||||
|
async doUpdateRequestInterception(): Promise<void> {
|
||||||
|
}
|
||||||
|
|
||||||
|
onClosePersistent() {}
|
||||||
|
|
||||||
|
override async clearCache(): Promise<void> {
|
||||||
|
}
|
||||||
|
|
||||||
|
async doClose(reason: string | undefined) {
|
||||||
|
// TODO: implement for persistent context
|
||||||
|
if (!this._browserContextId)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await this._browser._browserSession.send('browser.removeUserContext', {
|
||||||
|
userContext: this._browserContextId
|
||||||
|
});
|
||||||
|
this._browser._contexts.delete(this._browserContextId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async cancelDownload(uuid: string) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromBidiSameSite(sameSite: bidi.Network.SameSite): channels.NetworkCookie['sameSite'] {
|
||||||
|
switch (sameSite) {
|
||||||
|
case 'strict': return 'Strict';
|
||||||
|
case 'lax': return 'Lax';
|
||||||
|
case 'none': return 'None';
|
||||||
|
}
|
||||||
|
return 'None';
|
||||||
|
}
|
||||||
|
|
||||||
|
function toBidiSameSite(sameSite: channels.SetNetworkCookie['sameSite']): bidi.Network.SameSite {
|
||||||
|
switch (sameSite) {
|
||||||
|
case 'Strict': return bidi.Network.SameSite.Strict;
|
||||||
|
case 'Lax': return bidi.Network.SameSite.Lax;
|
||||||
|
case 'None': return bidi.Network.SameSite.None;
|
||||||
|
}
|
||||||
|
return bidi.Network.SameSite.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace Network {
|
||||||
|
export const enum SameSite {
|
||||||
|
Strict = 'strict',
|
||||||
|
Lax = 'lax',
|
||||||
|
None = 'none',
|
||||||
|
}
|
||||||
|
}
|
||||||
232
packages/playwright-core/src/server/bidi/bidiConnection.ts
Normal file
232
packages/playwright-core/src/server/bidi/bidiConnection.ts
Normal file
|
|
@ -0,0 +1,232 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the 'License');
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an 'AS IS' BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { EventEmitter } from 'events';
|
||||||
|
import { assert } from '../../utils';
|
||||||
|
import type { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport';
|
||||||
|
import type { RecentLogsCollector } from '../../utils/debugLogger';
|
||||||
|
import { debugLogger } from '../../utils/debugLogger';
|
||||||
|
import type { ProtocolLogger } from '../types';
|
||||||
|
import { helper } from '../helper';
|
||||||
|
import { ProtocolError } from '../protocolError';
|
||||||
|
import type * as bidi from './third_party/bidiProtocol';
|
||||||
|
import type * as bidiCommands from './third_party/bidiCommands';
|
||||||
|
|
||||||
|
// BidiPlaywright uses this special id to issue Browser.close command which we
|
||||||
|
// should ignore.
|
||||||
|
export const kBrowserCloseMessageId = 0;
|
||||||
|
|
||||||
|
export class BidiConnection {
|
||||||
|
private readonly _transport: ConnectionTransport;
|
||||||
|
private readonly _onDisconnect: () => void;
|
||||||
|
private readonly _protocolLogger: ProtocolLogger;
|
||||||
|
private readonly _browserLogsCollector: RecentLogsCollector;
|
||||||
|
_browserDisconnectedLogs: string | undefined;
|
||||||
|
private _lastId = 0;
|
||||||
|
private _closed = false;
|
||||||
|
readonly browserSession: BidiSession;
|
||||||
|
readonly _browsingContextToSession = new Map<string, BidiSession>();
|
||||||
|
|
||||||
|
constructor(transport: ConnectionTransport, onDisconnect: () => void, protocolLogger: ProtocolLogger, browserLogsCollector: RecentLogsCollector) {
|
||||||
|
this._transport = transport;
|
||||||
|
this._onDisconnect = onDisconnect;
|
||||||
|
this._protocolLogger = protocolLogger;
|
||||||
|
this._browserLogsCollector = browserLogsCollector;
|
||||||
|
this.browserSession = new BidiSession(this, '', (message: any) => {
|
||||||
|
this.rawSend(message);
|
||||||
|
});
|
||||||
|
this._transport.onmessage = this._dispatchMessage.bind(this);
|
||||||
|
// onclose should be set last, since it can be immediately called.
|
||||||
|
this._transport.onclose = this._onClose.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
nextMessageId(): number {
|
||||||
|
return ++this._lastId;
|
||||||
|
}
|
||||||
|
|
||||||
|
rawSend(message: ProtocolRequest) {
|
||||||
|
this._protocolLogger('send', message);
|
||||||
|
this._transport.send(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _dispatchMessage(message: ProtocolResponse) {
|
||||||
|
this._protocolLogger('receive', message);
|
||||||
|
const object = message as bidi.Message;
|
||||||
|
// Bidi messages do not have a common session identifier, so we
|
||||||
|
// route them based on BrowsingContext.
|
||||||
|
if (object.type === 'event') {
|
||||||
|
// Route page events to the right session.
|
||||||
|
let context;
|
||||||
|
if ('context' in object.params)
|
||||||
|
context = object.params.context;
|
||||||
|
else if (object.method === 'log.entryAdded')
|
||||||
|
context = object.params.source?.context;
|
||||||
|
if (context) {
|
||||||
|
const session = this._browsingContextToSession.get(context);
|
||||||
|
if (session) {
|
||||||
|
session.dispatchMessage(message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (message.id) {
|
||||||
|
// Find caller session.
|
||||||
|
for (const session of this._browsingContextToSession.values()) {
|
||||||
|
if (session.hasCallback(message.id)) {
|
||||||
|
session.dispatchMessage(message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.browserSession.dispatchMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onClose(reason?: string) {
|
||||||
|
this._closed = true;
|
||||||
|
this._transport.onmessage = undefined;
|
||||||
|
this._transport.onclose = undefined;
|
||||||
|
this._browserDisconnectedLogs = helper.formatBrowserLogs(this._browserLogsCollector.recentLogs(), reason);
|
||||||
|
this.browserSession.dispose();
|
||||||
|
this._onDisconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
isClosed() {
|
||||||
|
return this._closed;
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
if (!this._closed)
|
||||||
|
this._transport.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
createMainFrameBrowsingContextSession(bowsingContextId: bidi.BrowsingContext.BrowsingContext): BidiSession {
|
||||||
|
const result = new BidiSession(this, bowsingContextId, message => this.rawSend(message));
|
||||||
|
this._browsingContextToSession.set(bowsingContextId, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type BidiEvents = {
|
||||||
|
[K in bidi.Event['method']]: Extract<bidi.Event, {method: K}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class BidiSession extends EventEmitter {
|
||||||
|
readonly connection: BidiConnection;
|
||||||
|
readonly sessionId: string;
|
||||||
|
|
||||||
|
private _disposed = false;
|
||||||
|
private readonly _rawSend: (message: any) => void;
|
||||||
|
private readonly _callbacks = new Map<number, { resolve: (o: any) => void, reject: (e: ProtocolError) => void, error: ProtocolError }>();
|
||||||
|
private _crashed: boolean = false;
|
||||||
|
private readonly _browsingContexts = new Set<string>();
|
||||||
|
|
||||||
|
override on: <T extends keyof BidiEvents | symbol>(event: T, listener: (payload: T extends symbol ? any : BidiEvents[T extends keyof BidiEvents ? T : never]['params']) => void) => this;
|
||||||
|
override addListener: <T extends keyof BidiEvents | symbol>(event: T, listener: (payload: T extends symbol ? any : BidiEvents[T extends keyof BidiEvents ? T : never]['params']) => void) => this;
|
||||||
|
override off: <T extends keyof BidiEvents | symbol>(event: T, listener: (payload: T extends symbol ? any : BidiEvents[T extends keyof BidiEvents ? T : never]['params']) => void) => this;
|
||||||
|
override removeListener: <T extends keyof BidiEvents | symbol>(event: T, listener: (payload: T extends symbol ? any : BidiEvents[T extends keyof BidiEvents ? T : never]['params']) => void) => this;
|
||||||
|
override once: <T extends keyof BidiEvents | symbol>(event: T, listener: (payload: T extends symbol ? any : BidiEvents[T extends keyof BidiEvents ? T : never]['params']) => void) => this;
|
||||||
|
|
||||||
|
constructor(connection: BidiConnection, sessionId: string, rawSend: (message: any) => void) {
|
||||||
|
super();
|
||||||
|
this.setMaxListeners(0);
|
||||||
|
this.connection = connection;
|
||||||
|
this.sessionId = sessionId;
|
||||||
|
this._rawSend = rawSend;
|
||||||
|
|
||||||
|
this.on = super.on;
|
||||||
|
this.off = super.removeListener;
|
||||||
|
this.addListener = super.addListener;
|
||||||
|
this.removeListener = super.removeListener;
|
||||||
|
this.once = super.once;
|
||||||
|
}
|
||||||
|
|
||||||
|
addFrameBrowsingContext(context: string) {
|
||||||
|
this._browsingContexts.add(context);
|
||||||
|
this.connection._browsingContextToSession.set(context, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeFrameBrowsingContext(context: string) {
|
||||||
|
this._browsingContexts.delete(context);
|
||||||
|
this.connection._browsingContextToSession.delete(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
async send<T extends keyof bidiCommands.Commands>(
|
||||||
|
method: T,
|
||||||
|
params?: bidiCommands.Commands[T]['params']
|
||||||
|
): Promise<bidiCommands.Commands[T]['returnType']> {
|
||||||
|
if (this._crashed || this._disposed || this.connection._browserDisconnectedLogs)
|
||||||
|
throw new ProtocolError(this._crashed ? 'crashed' : 'closed', undefined, this.connection._browserDisconnectedLogs);
|
||||||
|
const id = this.connection.nextMessageId();
|
||||||
|
const messageObj = { id, method, params };
|
||||||
|
this._rawSend(messageObj);
|
||||||
|
return new Promise<bidiCommands.Commands[T]['returnType']>((resolve, reject) => {
|
||||||
|
this._callbacks.set(id, { resolve, reject, error: new ProtocolError('error', method) });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMayFail<T extends keyof bidiCommands.Commands>(method: T, params?: bidiCommands.Commands[T]['params']): Promise<bidiCommands.Commands[T]['returnType'] | void> {
|
||||||
|
return this.send(method, params).catch(error => debugLogger.log('error', error));
|
||||||
|
}
|
||||||
|
|
||||||
|
markAsCrashed() {
|
||||||
|
this._crashed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
isDisposed(): boolean {
|
||||||
|
return this._disposed;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
this._disposed = true;
|
||||||
|
this.connection._browsingContextToSession.delete(this.sessionId);
|
||||||
|
for (const context of this._browsingContexts)
|
||||||
|
this.connection._browsingContextToSession.delete(context);
|
||||||
|
this._browsingContexts.clear();
|
||||||
|
for (const callback of this._callbacks.values()) {
|
||||||
|
callback.error.type = this._crashed ? 'crashed' : 'closed';
|
||||||
|
callback.error.logs = this.connection._browserDisconnectedLogs;
|
||||||
|
callback.reject(callback.error);
|
||||||
|
}
|
||||||
|
this._callbacks.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
hasCallback(id: number): boolean {
|
||||||
|
return this._callbacks.has(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatchMessage(message: any) {
|
||||||
|
const object = message as bidi.Message;
|
||||||
|
if (object.id === kBrowserCloseMessageId)
|
||||||
|
return;
|
||||||
|
if (object.id && this._callbacks.has(object.id)) {
|
||||||
|
const callback = this._callbacks.get(object.id)!;
|
||||||
|
this._callbacks.delete(object.id);
|
||||||
|
if (object.type === 'error') {
|
||||||
|
callback.error.setMessage(object.error + '\nMessage: ' + object.message);
|
||||||
|
callback.reject(callback.error);
|
||||||
|
} else if (object.type === 'success') {
|
||||||
|
callback.resolve(object.result);
|
||||||
|
} else {
|
||||||
|
callback.error.setMessage('Internal error, unexpected response type: ' + JSON.stringify(object));
|
||||||
|
callback.reject(callback.error);
|
||||||
|
}
|
||||||
|
} else if (object.id) {
|
||||||
|
// Response might come after session has been disposed and rejected all callbacks.
|
||||||
|
assert(this.isDisposed());
|
||||||
|
} else {
|
||||||
|
Promise.resolve().then(() => this.emit(object.method, object.params));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
167
packages/playwright-core/src/server/bidi/bidiExecutionContext.ts
Normal file
167
packages/playwright-core/src/server/bidi/bidiExecutionContext.ts
Normal file
|
|
@ -0,0 +1,167 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the 'License');
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an 'AS IS' BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { parseEvaluationResultValue } from '../isomorphic/utilityScriptSerializers';
|
||||||
|
import * as js from '../javascript';
|
||||||
|
import type { BidiSession } from './bidiConnection';
|
||||||
|
import { BidiDeserializer } from './third_party/bidiDeserializer';
|
||||||
|
import * as bidi from './third_party/bidiProtocol';
|
||||||
|
import { BidiSerializer } from './third_party/bidiSerializer';
|
||||||
|
|
||||||
|
export class BidiExecutionContext implements js.ExecutionContextDelegate {
|
||||||
|
private readonly _session: BidiSession;
|
||||||
|
private readonly _target: bidi.Script.Target;
|
||||||
|
|
||||||
|
constructor(session: BidiSession, realmInfo: bidi.Script.RealmInfo) {
|
||||||
|
this._session = session;
|
||||||
|
if (realmInfo.type === 'window') {
|
||||||
|
// Simple realm does not seem to work for Window contexts.
|
||||||
|
this._target = {
|
||||||
|
context: realmInfo.context,
|
||||||
|
sandbox: realmInfo.sandbox,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
this._target = {
|
||||||
|
realm: realmInfo.realm
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async rawEvaluateJSON(expression: string): Promise<any> {
|
||||||
|
const response = await this._session.send('script.evaluate', {
|
||||||
|
expression,
|
||||||
|
target: this._target,
|
||||||
|
serializationOptions: {
|
||||||
|
maxObjectDepth: 10,
|
||||||
|
maxDomDepth: 10,
|
||||||
|
},
|
||||||
|
awaitPromise: true,
|
||||||
|
userActivation: true,
|
||||||
|
});
|
||||||
|
if (response.type === 'success')
|
||||||
|
return BidiDeserializer.deserialize(response.result);
|
||||||
|
if (response.type === 'exception')
|
||||||
|
throw new js.JavaScriptErrorInEvaluate(response.exceptionDetails.text + '\nFull val: ' + JSON.stringify(response.exceptionDetails));
|
||||||
|
throw new js.JavaScriptErrorInEvaluate('Unexpected response type: ' + JSON.stringify(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
async rawEvaluateHandle(expression: string): Promise<js.ObjectId> {
|
||||||
|
const response = await this._session.send('script.evaluate', {
|
||||||
|
expression,
|
||||||
|
target: this._target,
|
||||||
|
resultOwnership: bidi.Script.ResultOwnership.Root, // Necessary for the handle to be returned.
|
||||||
|
serializationOptions: { maxObjectDepth: 0, maxDomDepth: 0 },
|
||||||
|
awaitPromise: true,
|
||||||
|
userActivation: true,
|
||||||
|
});
|
||||||
|
if (response.type === 'success') {
|
||||||
|
if ('handle' in response.result)
|
||||||
|
return response.result.handle!;
|
||||||
|
throw new js.JavaScriptErrorInEvaluate('Cannot get handle: ' + JSON.stringify(response.result));
|
||||||
|
}
|
||||||
|
if (response.type === 'exception')
|
||||||
|
throw new js.JavaScriptErrorInEvaluate(response.exceptionDetails.text + '\nFull val: ' + JSON.stringify(response.exceptionDetails));
|
||||||
|
throw new js.JavaScriptErrorInEvaluate('Unexpected response type: ' + JSON.stringify(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
rawCallFunctionNoReply(func: Function, ...args: any[]) {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async evaluateWithArguments(functionDeclaration: string, returnByValue: boolean, utilityScript: js.JSHandle<any>, values: any[], objectIds: string[]): Promise<any> {
|
||||||
|
const response = await this._session.send('script.callFunction', {
|
||||||
|
functionDeclaration,
|
||||||
|
target: this._target,
|
||||||
|
arguments: [
|
||||||
|
{ handle: utilityScript._objectId! },
|
||||||
|
...values.map(BidiSerializer.serialize),
|
||||||
|
...objectIds.map(handle => ({ handle })),
|
||||||
|
],
|
||||||
|
resultOwnership: returnByValue ? undefined : bidi.Script.ResultOwnership.Root, // Necessary for the handle to be returned.
|
||||||
|
serializationOptions: returnByValue ? {} : { maxObjectDepth: 0, maxDomDepth: 0 },
|
||||||
|
awaitPromise: true,
|
||||||
|
userActivation: true,
|
||||||
|
});
|
||||||
|
if (response.type === 'exception')
|
||||||
|
throw new js.JavaScriptErrorInEvaluate(response.exceptionDetails.text + '\nFull val: ' + JSON.stringify(response.exceptionDetails));
|
||||||
|
if (response.type === 'success') {
|
||||||
|
if (returnByValue)
|
||||||
|
return parseEvaluationResultValue(BidiDeserializer.deserialize(response.result));
|
||||||
|
const objectId = 'handle' in response.result ? response.result.handle : undefined ;
|
||||||
|
return utilityScript._context.createHandle({ objectId, ...response.result });
|
||||||
|
}
|
||||||
|
throw new js.JavaScriptErrorInEvaluate('Unexpected response type: ' + JSON.stringify(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
async getProperties(context: js.ExecutionContext, objectId: js.ObjectId): Promise<Map<string, js.JSHandle>> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
createHandle(context: js.ExecutionContext, jsRemoteObject: js.RemoteObject): js.JSHandle {
|
||||||
|
const remoteObject: bidi.Script.RemoteValue = jsRemoteObject as bidi.Script.RemoteValue;
|
||||||
|
return new js.JSHandle(context, remoteObject.type, renderPreview(remoteObject), jsRemoteObject.objectId, remoteObjectValue(remoteObject));
|
||||||
|
}
|
||||||
|
|
||||||
|
async releaseHandle(objectId: js.ObjectId): Promise<void> {
|
||||||
|
await this._session.send('script.disown', {
|
||||||
|
target: this._target,
|
||||||
|
handles: [objectId],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
objectCount(objectId: js.ObjectId): Promise<number> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async rawCallFunction(functionDeclaration: string, arg: bidi.Script.LocalValue): Promise<bidi.Script.RemoteValue> {
|
||||||
|
const response = await this._session.send('script.callFunction', {
|
||||||
|
functionDeclaration,
|
||||||
|
target: this._target,
|
||||||
|
arguments: [arg],
|
||||||
|
resultOwnership: bidi.Script.ResultOwnership.Root, // Necessary for the handle to be returned.
|
||||||
|
serializationOptions: { maxObjectDepth: 0, maxDomDepth: 0 },
|
||||||
|
awaitPromise: true,
|
||||||
|
userActivation: true,
|
||||||
|
});
|
||||||
|
if (response.type === 'exception')
|
||||||
|
throw new js.JavaScriptErrorInEvaluate(response.exceptionDetails.text + '\nFull val: ' + JSON.stringify(response.exceptionDetails));
|
||||||
|
if (response.type === 'success')
|
||||||
|
return response.result;
|
||||||
|
throw new js.JavaScriptErrorInEvaluate('Unexpected response type: ' + JSON.stringify(response));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPreview(remoteObject: bidi.Script.RemoteValue): string | undefined {
|
||||||
|
if (remoteObject.type === 'undefined')
|
||||||
|
return 'undefined';
|
||||||
|
if (remoteObject.type === 'null')
|
||||||
|
return 'null';
|
||||||
|
if ('value' in remoteObject)
|
||||||
|
return String(remoteObject.value);
|
||||||
|
return `<${remoteObject.type}>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function remoteObjectValue(remoteObject: bidi.Script.RemoteValue): any {
|
||||||
|
if (remoteObject.type === 'undefined')
|
||||||
|
return undefined;
|
||||||
|
if (remoteObject.type === 'null')
|
||||||
|
return null;
|
||||||
|
if (remoteObject.type === 'number' && typeof remoteObject.value === 'string')
|
||||||
|
return js.parseUnserializableValue(remoteObject.value);
|
||||||
|
if ('value' in remoteObject)
|
||||||
|
return remoteObject.value;
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
112
packages/playwright-core/src/server/bidi/bidiFirefox.ts
Normal file
112
packages/playwright-core/src/server/bidi/bidiFirefox.ts
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the 'License');
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an 'AS IS' BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import os from 'os';
|
||||||
|
import path from 'path';
|
||||||
|
import { assert, ManualPromise, wrapInASCIIBox } from '../../utils';
|
||||||
|
import type { Env } from '../../utils/processLauncher';
|
||||||
|
import type { BrowserOptions } from '../browser';
|
||||||
|
import type { BrowserReadyState } from '../browserType';
|
||||||
|
import { BrowserType, kNoXServerRunningError } from '../browserType';
|
||||||
|
import type { SdkObject } from '../instrumentation';
|
||||||
|
import type { ProtocolError } from '../protocolError';
|
||||||
|
import type { ConnectionTransport } from '../transport';
|
||||||
|
import type * as types from '../types';
|
||||||
|
import { BidiBrowser } from './bidiBrowser';
|
||||||
|
import { kBrowserCloseMessageId } from './bidiConnection';
|
||||||
|
|
||||||
|
export class BidiFirefox extends BrowserType {
|
||||||
|
constructor(parent: SdkObject) {
|
||||||
|
super(parent, 'bidi');
|
||||||
|
this._useBidi = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
override async connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise<BidiBrowser> {
|
||||||
|
return BidiBrowser.connect(this.attribution.playwright, transport, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
override doRewriteStartupLog(error: ProtocolError): ProtocolError {
|
||||||
|
if (!error.logs)
|
||||||
|
return error;
|
||||||
|
// https://github.com/microsoft/playwright/issues/6500
|
||||||
|
if (error.logs.includes(`as root in a regular user's session is not supported.`))
|
||||||
|
error.logs = '\n' + wrapInASCIIBox(`Firefox is unable to launch if the $HOME folder isn't owned by the current user.\nWorkaround: Set the HOME=/root environment variable${process.env.GITHUB_ACTION ? ' in your GitHub Actions workflow file' : ''} when running Playwright.`, 1);
|
||||||
|
if (error.logs.includes('no DISPLAY environment variable specified'))
|
||||||
|
error.logs = '\n' + wrapInASCIIBox(kNoXServerRunningError, 1);
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
override amendEnvironment(env: Env, userDataDir: string, executable: string, browserArguments: string[]): Env {
|
||||||
|
if (!path.isAbsolute(os.homedir()))
|
||||||
|
throw new Error(`Cannot launch Firefox with relative home directory. Did you set ${os.platform() === 'win32' ? 'USERPROFILE' : 'HOME'} to a relative path?`);
|
||||||
|
if (os.platform() === 'linux') {
|
||||||
|
// Always remove SNAP_NAME and SNAP_INSTANCE_NAME env variables since they
|
||||||
|
// confuse Firefox: in our case, builds never come from SNAP.
|
||||||
|
// See https://github.com/microsoft/playwright/issues/20555
|
||||||
|
return { ...env, SNAP_NAME: undefined, SNAP_INSTANCE_NAME: undefined };
|
||||||
|
}
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
|
||||||
|
override attemptToGracefullyCloseBrowser(transport: ConnectionTransport): void {
|
||||||
|
transport.send({ method: 'browser.close', params: {}, id: kBrowserCloseMessageId });
|
||||||
|
}
|
||||||
|
|
||||||
|
override defaultArgs(options: types.LaunchOptions, isPersistent: boolean, userDataDir: string): string[] {
|
||||||
|
const { args = [], headless } = options;
|
||||||
|
const userDataDirArg = args.find(arg => arg.startsWith('-profile') || arg.startsWith('--profile'));
|
||||||
|
if (userDataDirArg)
|
||||||
|
throw this._createUserDataDirArgMisuseError('--profile');
|
||||||
|
const firefoxArguments = ['--remote-debugging-port=0'];
|
||||||
|
if (headless)
|
||||||
|
firefoxArguments.push('--headless');
|
||||||
|
else
|
||||||
|
firefoxArguments.push('--foreground');
|
||||||
|
firefoxArguments.push(`--profile`, userDataDir);
|
||||||
|
firefoxArguments.push(...args);
|
||||||
|
// TODO: make ephemeral context work without this argument.
|
||||||
|
firefoxArguments.push('about:blank');
|
||||||
|
// if (isPersistent)
|
||||||
|
// firefoxArguments.push('about:blank');
|
||||||
|
// else
|
||||||
|
// firefoxArguments.push('-silent');
|
||||||
|
return firefoxArguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
override readyState(options: types.LaunchOptions): BrowserReadyState | undefined {
|
||||||
|
assert(options.useWebSocket);
|
||||||
|
return new BidiReadyState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BidiReadyState implements BrowserReadyState {
|
||||||
|
private readonly _wsEndpoint = new ManualPromise<string|undefined>();
|
||||||
|
|
||||||
|
onBrowserOutput(message: string): void {
|
||||||
|
// Bidi WebSocket in Firefox.
|
||||||
|
const match = message.match(/WebDriver BiDi listening on (ws:\/\/.*)$/);
|
||||||
|
if (match)
|
||||||
|
this._wsEndpoint.resolve(match[1] + '/session');
|
||||||
|
}
|
||||||
|
onBrowserExit(): void {
|
||||||
|
// Unblock launch when browser prematurely exits.
|
||||||
|
this._wsEndpoint.resolve(undefined);
|
||||||
|
}
|
||||||
|
async waitUntilReady(): Promise<{ wsEndpoint?: string }> {
|
||||||
|
const wsEndpoint = await this._wsEndpoint;
|
||||||
|
return { wsEndpoint };
|
||||||
|
}
|
||||||
|
}
|
||||||
149
packages/playwright-core/src/server/bidi/bidiInput.ts
Normal file
149
packages/playwright-core/src/server/bidi/bidiInput.ts
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the 'License');
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an 'AS IS' BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type * as input from '../input';
|
||||||
|
import type * as types from '../types';
|
||||||
|
import type { BidiSession } from './bidiConnection';
|
||||||
|
import * as bidi from './third_party/bidiProtocol';
|
||||||
|
import { getBidiKeyValue } from './third_party/bidiKeyboard';
|
||||||
|
|
||||||
|
export class RawKeyboardImpl implements input.RawKeyboard {
|
||||||
|
private _session: BidiSession;
|
||||||
|
|
||||||
|
constructor(session: BidiSession) {
|
||||||
|
this._session = session;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSession(session: BidiSession) {
|
||||||
|
this._session = session;
|
||||||
|
}
|
||||||
|
|
||||||
|
async keydown(modifiers: Set<types.KeyboardModifier>, code: string, keyCode: number, keyCodeWithoutLocation: number, key: string, location: number, autoRepeat: boolean, text: string | undefined): Promise<void> {
|
||||||
|
const actions: bidi.Input.KeySourceAction[] = [];
|
||||||
|
actions.push({ type: 'keyDown', value: getBidiKeyValue(key) });
|
||||||
|
// TODO: add modifiers?
|
||||||
|
await this._performActions(actions);
|
||||||
|
}
|
||||||
|
|
||||||
|
async keyup(modifiers: Set<types.KeyboardModifier>, code: string, keyCode: number, keyCodeWithoutLocation: number, key: string, location: number): Promise<void> {
|
||||||
|
const actions: bidi.Input.KeySourceAction[] = [];
|
||||||
|
actions.push({ type: 'keyUp', value: getBidiKeyValue(key) });
|
||||||
|
await this._performActions(actions);
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendText(text: string): Promise<void> {
|
||||||
|
const actions: bidi.Input.KeySourceAction[] = [];
|
||||||
|
for (const char of text) {
|
||||||
|
const value = getBidiKeyValue(char);
|
||||||
|
actions.push({ type: 'keyDown', value });
|
||||||
|
actions.push({ type: 'keyUp', value });
|
||||||
|
}
|
||||||
|
await this._performActions(actions);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _performActions(actions: bidi.Input.KeySourceAction[]) {
|
||||||
|
await this._session.send('input.performActions', {
|
||||||
|
context: this._session.sessionId,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
type: 'key',
|
||||||
|
id: 'pw_keyboard',
|
||||||
|
actions,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RawMouseImpl implements input.RawMouse {
|
||||||
|
private readonly _session: BidiSession;
|
||||||
|
|
||||||
|
constructor(session: BidiSession) {
|
||||||
|
this._session = session;
|
||||||
|
}
|
||||||
|
|
||||||
|
async move(x: number, y: number, button: types.MouseButton | 'none', buttons: Set<types.MouseButton>, modifiers: Set<types.KeyboardModifier>, forClick: boolean): Promise<void> {
|
||||||
|
// TODO: bidi throws when x/y are not integers.
|
||||||
|
x = Math.round(x);
|
||||||
|
y = Math.round(y);
|
||||||
|
await this._performActions([{ type: 'pointerMove', x, y }]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(x: number, y: number, button: types.MouseButton, buttons: Set<types.MouseButton>, modifiers: Set<types.KeyboardModifier>, clickCount: number): Promise<void> {
|
||||||
|
await this._performActions([{ type: 'pointerDown', button: toBidiButton(button) }]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async up(x: number, y: number, button: types.MouseButton, buttons: Set<types.MouseButton>, modifiers: Set<types.KeyboardModifier>, clickCount: number): Promise<void> {
|
||||||
|
await this._performActions([{ type: 'pointerUp', button: toBidiButton(button) }]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async click(x: number, y: number, options: { delay?: number, button?: types.MouseButton, clickCount?: number } = {}) {
|
||||||
|
x = Math.round(x);
|
||||||
|
y = Math.round(y);
|
||||||
|
const button = toBidiButton(options.button || 'left');
|
||||||
|
const { delay = null, clickCount = 1 } = options;
|
||||||
|
const actions: bidi.Input.PointerSourceAction[] = [];
|
||||||
|
actions.push({ type: 'pointerMove', x, y });
|
||||||
|
for (let cc = 1; cc <= clickCount; ++cc) {
|
||||||
|
actions.push({ type: 'pointerDown', button });
|
||||||
|
if (delay)
|
||||||
|
actions.push({ type: 'pause', duration: delay });
|
||||||
|
actions.push({ type: 'pointerUp', button });
|
||||||
|
if (delay && cc < clickCount)
|
||||||
|
actions.push({ type: 'pause', duration: delay });
|
||||||
|
}
|
||||||
|
await this._performActions(actions);
|
||||||
|
}
|
||||||
|
|
||||||
|
async wheel(x: number, y: number, buttons: Set<types.MouseButton>, modifiers: Set<types.KeyboardModifier>, deltaX: number, deltaY: number): Promise<void> {
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _performActions(actions: bidi.Input.PointerSourceAction[]) {
|
||||||
|
await this._session.send('input.performActions', {
|
||||||
|
context: this._session.sessionId,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
type: 'pointer',
|
||||||
|
id: 'pw_mouse',
|
||||||
|
parameters: {
|
||||||
|
pointerType: bidi.Input.PointerType.Mouse,
|
||||||
|
},
|
||||||
|
actions,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RawTouchscreenImpl implements input.RawTouchscreen {
|
||||||
|
private readonly _session: BidiSession;
|
||||||
|
|
||||||
|
constructor(session: BidiSession) {
|
||||||
|
this._session = session;
|
||||||
|
}
|
||||||
|
|
||||||
|
async tap(x: number, y: number, modifiers: Set<types.KeyboardModifier>) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toBidiButton(button: string): number {
|
||||||
|
switch (button) {
|
||||||
|
case 'left': return 0;
|
||||||
|
case 'right': return 2;
|
||||||
|
case 'middle': return 1;
|
||||||
|
}
|
||||||
|
throw new Error('Unknown button: ' + button);
|
||||||
|
}
|
||||||
316
packages/playwright-core/src/server/bidi/bidiNetworkManager.ts
Normal file
316
packages/playwright-core/src/server/bidi/bidiNetworkManager.ts
Normal file
|
|
@ -0,0 +1,316 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the 'License');
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an 'AS IS' BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { RegisteredListener } from '../../utils/eventsHelper';
|
||||||
|
import { eventsHelper } from '../../utils/eventsHelper';
|
||||||
|
import type { Page } from '../page';
|
||||||
|
import * as network from '../network';
|
||||||
|
import type * as frames from '../frames';
|
||||||
|
import type * as types from '../types';
|
||||||
|
import * as bidi from './third_party/bidiProtocol';
|
||||||
|
import type { BidiSession } from './bidiConnection';
|
||||||
|
|
||||||
|
|
||||||
|
export class BidiNetworkManager {
|
||||||
|
private readonly _session: BidiSession;
|
||||||
|
private readonly _requests: Map<string, BidiRequest>;
|
||||||
|
private readonly _page: Page;
|
||||||
|
private readonly _eventListeners: RegisteredListener[];
|
||||||
|
private readonly _onNavigationResponseStarted: (params: bidi.Network.ResponseStartedParameters) => void;
|
||||||
|
private _userRequestInterceptionEnabled: boolean = false;
|
||||||
|
private _protocolRequestInterceptionEnabled: boolean = false;
|
||||||
|
private _credentials: types.Credentials | undefined;
|
||||||
|
private _intercepId: bidi.Network.Intercept | undefined;
|
||||||
|
|
||||||
|
constructor(bidiSession: BidiSession, page: Page, onNavigationResponseStarted: (params: bidi.Network.ResponseStartedParameters) => void) {
|
||||||
|
this._session = bidiSession;
|
||||||
|
this._requests = new Map();
|
||||||
|
this._page = page;
|
||||||
|
this._onNavigationResponseStarted = onNavigationResponseStarted;
|
||||||
|
this._eventListeners = [
|
||||||
|
eventsHelper.addEventListener(bidiSession, 'network.beforeRequestSent', this._onBeforeRequestSent.bind(this)),
|
||||||
|
eventsHelper.addEventListener(bidiSession, 'network.responseStarted', this._onResponseStarted.bind(this)),
|
||||||
|
eventsHelper.addEventListener(bidiSession, 'network.responseCompleted', this._onResponseCompleted.bind(this)),
|
||||||
|
eventsHelper.addEventListener(bidiSession, 'network.fetchError', this._onFetchError.bind(this)),
|
||||||
|
eventsHelper.addEventListener(bidiSession, 'network.authRequired', this._onAuthRequired.bind(this)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
eventsHelper.removeEventListeners(this._eventListeners);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onBeforeRequestSent(param: bidi.Network.BeforeRequestSentParameters) {
|
||||||
|
if (param.request.url.startsWith('data:'))
|
||||||
|
return;
|
||||||
|
const redirectedFrom = param.redirectCount ? (this._requests.get(param.request.request) || null) : null;
|
||||||
|
const frame = redirectedFrom ? redirectedFrom.request.frame() : (param.context ? this._page._frameManager.frame(param.context) : null);
|
||||||
|
if (!frame)
|
||||||
|
return;
|
||||||
|
if (redirectedFrom)
|
||||||
|
this._requests.delete(redirectedFrom._id);
|
||||||
|
let route;
|
||||||
|
if (param.intercepts) {
|
||||||
|
// We do not support intercepting redirects.
|
||||||
|
if (redirectedFrom) {
|
||||||
|
this._session.sendMayFail('network.continueRequest', {
|
||||||
|
request: param.request.request,
|
||||||
|
headers: redirectedFrom._originalRequestRoute?._alreadyContinuedHeaders,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
route = new BidiRouteImpl(this._session, param.request.request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const request = new BidiRequest(frame, redirectedFrom, param, route);
|
||||||
|
this._requests.set(request._id, request);
|
||||||
|
this._page._frameManager.requestStarted(request.request, route);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onResponseStarted(params: bidi.Network.ResponseStartedParameters) {
|
||||||
|
const request = this._requests.get(params.request.request);
|
||||||
|
if (!request)
|
||||||
|
return;
|
||||||
|
const getResponseBody = async () => {
|
||||||
|
throw new Error(`Response body is not available for requests in Bidi`);
|
||||||
|
};
|
||||||
|
const timings = params.request.timings;
|
||||||
|
const startTime = timings.requestTime;
|
||||||
|
function relativeToStart(time: number): number {
|
||||||
|
if (!time)
|
||||||
|
return -1;
|
||||||
|
return (time - startTime) / 1000;
|
||||||
|
}
|
||||||
|
const timing: network.ResourceTiming = {
|
||||||
|
startTime: startTime / 1000,
|
||||||
|
requestStart: relativeToStart(timings.requestStart),
|
||||||
|
responseStart: relativeToStart(timings.responseStart),
|
||||||
|
domainLookupStart: relativeToStart(timings.dnsStart),
|
||||||
|
domainLookupEnd: relativeToStart(timings.dnsEnd),
|
||||||
|
connectStart: relativeToStart(timings.connectStart),
|
||||||
|
secureConnectionStart: relativeToStart(timings.tlsStart),
|
||||||
|
connectEnd: relativeToStart(timings.connectEnd),
|
||||||
|
};
|
||||||
|
const response = new network.Response(request.request, params.response.status, params.response.statusText, fromBidiHeaders(params.response.headers), timing, getResponseBody, false);
|
||||||
|
response._serverAddrFinished();
|
||||||
|
response._securityDetailsFinished();
|
||||||
|
// "raw" headers are the same as "provisional" headers in Bidi.
|
||||||
|
response.setRawResponseHeaders(null);
|
||||||
|
response.setResponseHeadersSize(params.response.headersSize);
|
||||||
|
this._page._frameManager.requestReceivedResponse(response);
|
||||||
|
if (params.navigation)
|
||||||
|
this._onNavigationResponseStarted(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onResponseCompleted(params: bidi.Network.ResponseCompletedParameters) {
|
||||||
|
const request = this._requests.get(params.request.request);
|
||||||
|
if (!request)
|
||||||
|
return;
|
||||||
|
const response = request.request._existingResponse()!;
|
||||||
|
// TODO: body size is the encoded size
|
||||||
|
response.setTransferSize(params.response.bodySize);
|
||||||
|
response.setEncodedBodySize(params.response.bodySize);
|
||||||
|
|
||||||
|
// Keep redirected requests in the map for future reference as redirectedFrom.
|
||||||
|
const isRedirected = response.status() >= 300 && response.status() <= 399;
|
||||||
|
const responseEndTime = params.request.timings.responseEnd / 1000 - response.timing().startTime;
|
||||||
|
if (isRedirected) {
|
||||||
|
response._requestFinished(responseEndTime);
|
||||||
|
} else {
|
||||||
|
this._requests.delete(request._id);
|
||||||
|
response._requestFinished(responseEndTime);
|
||||||
|
}
|
||||||
|
response._setHttpVersion(params.response.protocol);
|
||||||
|
this._page._frameManager.reportRequestFinished(request.request, response);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onFetchError(params: bidi.Network.FetchErrorParameters) {
|
||||||
|
const request = this._requests.get(params.request.request);
|
||||||
|
if (!request)
|
||||||
|
return;
|
||||||
|
this._requests.delete(request._id);
|
||||||
|
const response = request.request._existingResponse();
|
||||||
|
if (response) {
|
||||||
|
response.setTransferSize(null);
|
||||||
|
response.setEncodedBodySize(null);
|
||||||
|
response._requestFinished(-1);
|
||||||
|
}
|
||||||
|
request.request._setFailureText(params.errorText);
|
||||||
|
// TODO: support canceled flag
|
||||||
|
this._page._frameManager.requestFailed(request.request, params.errorText === 'NS_BINDING_ABORTED');
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onAuthRequired(params: bidi.Network.AuthRequiredParameters) {
|
||||||
|
const isBasic = params.response.authChallenges?.some(challenge => challenge.scheme.startsWith('Basic'));
|
||||||
|
const credentials = this._page._browserContext._options.httpCredentials;
|
||||||
|
if (isBasic && credentials) {
|
||||||
|
this._session.sendMayFail('network.continueWithAuth', {
|
||||||
|
request: params.request.request,
|
||||||
|
action: 'provideCredentials',
|
||||||
|
credentials: {
|
||||||
|
type: 'password',
|
||||||
|
username: credentials.username,
|
||||||
|
password: credentials.password,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this._session.sendMayFail('network.continueWithAuth', {
|
||||||
|
request: params.request.request,
|
||||||
|
action: 'default',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async setRequestInterception(value: boolean) {
|
||||||
|
this._userRequestInterceptionEnabled = value;
|
||||||
|
await this._updateProtocolRequestInterception();
|
||||||
|
}
|
||||||
|
|
||||||
|
async setCredentials(credentials: types.Credentials | undefined) {
|
||||||
|
this._credentials = credentials;
|
||||||
|
await this._updateProtocolRequestInterception();
|
||||||
|
}
|
||||||
|
|
||||||
|
async _updateProtocolRequestInterception(initial?: boolean) {
|
||||||
|
const enabled = this._userRequestInterceptionEnabled || !!this._credentials;
|
||||||
|
if (enabled === this._protocolRequestInterceptionEnabled)
|
||||||
|
return;
|
||||||
|
this._protocolRequestInterceptionEnabled = enabled;
|
||||||
|
if (initial && !enabled)
|
||||||
|
return;
|
||||||
|
const cachePromise = this._session.send('network.setCacheBehavior', { cacheBehavior: enabled ? 'bypass' : 'default' });
|
||||||
|
let interceptPromise = Promise.resolve<any>(undefined);
|
||||||
|
if (enabled) {
|
||||||
|
interceptPromise = this._session.send('network.addIntercept', {
|
||||||
|
phases: [bidi.Network.InterceptPhase.AuthRequired, bidi.Network.InterceptPhase.BeforeRequestSent],
|
||||||
|
urlPatterns: [{ type: 'pattern' }],
|
||||||
|
// urlPatterns: [{ type: 'string', pattern: '*' }],
|
||||||
|
}).then(r => {
|
||||||
|
this._intercepId = r.intercept;
|
||||||
|
});
|
||||||
|
} else if (this._intercepId) {
|
||||||
|
interceptPromise = this._session.send('network.removeIntercept', { intercept: this._intercepId });
|
||||||
|
this._intercepId = undefined;
|
||||||
|
}
|
||||||
|
await Promise.all([cachePromise, interceptPromise]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class BidiRequest {
|
||||||
|
readonly request: network.Request;
|
||||||
|
readonly _id: string;
|
||||||
|
private _redirectedTo: BidiRequest | undefined;
|
||||||
|
// Only first request in the chain can be intercepted, so this will
|
||||||
|
// store the first and only Route in the chain (if any).
|
||||||
|
_originalRequestRoute: BidiRouteImpl | undefined;
|
||||||
|
|
||||||
|
constructor(frame: frames.Frame, redirectedFrom: BidiRequest | null, payload: bidi.Network.BeforeRequestSentParameters, route: BidiRouteImpl | undefined) {
|
||||||
|
this._id = payload.request.request;
|
||||||
|
if (redirectedFrom)
|
||||||
|
redirectedFrom._redirectedTo = this;
|
||||||
|
// TODO: missing in the spec?
|
||||||
|
const postDataBuffer = null;
|
||||||
|
this.request = new network.Request(frame._page._browserContext, frame, null, redirectedFrom ? redirectedFrom.request : null, payload.navigation ?? undefined,
|
||||||
|
payload.request.url, 'other', payload.request.method, postDataBuffer, fromBidiHeaders(payload.request.headers));
|
||||||
|
// "raw" headers are the same as "provisional" headers in Bidi.
|
||||||
|
this.request.setRawRequestHeaders(null);
|
||||||
|
this.request._setBodySize(payload.request.bodySize || 0);
|
||||||
|
this._originalRequestRoute = route ?? redirectedFrom?._originalRequestRoute;
|
||||||
|
route?._setRequest(this.request);
|
||||||
|
}
|
||||||
|
|
||||||
|
_finalRequest(): BidiRequest {
|
||||||
|
let request: BidiRequest = this;
|
||||||
|
while (request._redirectedTo)
|
||||||
|
request = request._redirectedTo;
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BidiRouteImpl implements network.RouteDelegate {
|
||||||
|
private _requestId: bidi.Network.Request;
|
||||||
|
private _session: BidiSession;
|
||||||
|
private _request!: network.Request;
|
||||||
|
_alreadyContinuedHeaders: bidi.Network.Header[] | undefined;
|
||||||
|
|
||||||
|
constructor(session: BidiSession, requestId: bidi.Network.Request) {
|
||||||
|
this._session = session;
|
||||||
|
this._requestId = requestId;
|
||||||
|
}
|
||||||
|
|
||||||
|
_setRequest(request: network.Request) {
|
||||||
|
this._request = request;
|
||||||
|
}
|
||||||
|
|
||||||
|
async continue(overrides: types.NormalizedContinueOverrides) {
|
||||||
|
// Firefox does not update content-length header.
|
||||||
|
let headers = overrides.headers || this._request.headers();
|
||||||
|
if (overrides.postData && headers) {
|
||||||
|
headers = headers.map(header => {
|
||||||
|
if (header.name.toLowerCase() === 'content-length')
|
||||||
|
return { name: header.name, value: overrides.postData!.byteLength.toString() };
|
||||||
|
return header;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this._alreadyContinuedHeaders = toBidiHeaders(headers);
|
||||||
|
await this._session.sendMayFail('network.continueRequest', {
|
||||||
|
request: this._requestId,
|
||||||
|
url: overrides.url,
|
||||||
|
method: overrides.method,
|
||||||
|
// TODO: cookies!
|
||||||
|
headers: this._alreadyContinuedHeaders,
|
||||||
|
body: overrides.postData ? { type: 'base64', value: Buffer.from(overrides.postData).toString('base64') } : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async fulfill(response: types.NormalizedFulfillResponse) {
|
||||||
|
const base64body = response.isBase64 ? response.body : Buffer.from(response.body).toString('base64');
|
||||||
|
await this._session.sendMayFail('network.provideResponse', {
|
||||||
|
request: this._requestId,
|
||||||
|
statusCode: response.status,
|
||||||
|
reasonPhrase: network.statusText(response.status),
|
||||||
|
headers: toBidiHeaders(response.headers),
|
||||||
|
body: { type: 'base64', value: base64body },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async abort(errorCode: string) {
|
||||||
|
await this._session.sendMayFail('network.failRequest', {
|
||||||
|
request: this._requestId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromBidiHeaders(bidiHeaders: bidi.Network.Header[]): types.HeadersArray {
|
||||||
|
const result: types.HeadersArray = [];
|
||||||
|
for (const { name, value } of bidiHeaders)
|
||||||
|
result.push({ name, value: bidiBytesValueToString(value) });
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toBidiHeaders(headers: types.HeadersArray): bidi.Network.Header[] {
|
||||||
|
return headers.map(({ name, value }) => ({ name, value: { type: 'string', value } }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bidiBytesValueToString(value: bidi.Network.BytesValue): string {
|
||||||
|
if (value.type === 'string')
|
||||||
|
return value.value;
|
||||||
|
if (value.type === 'base64')
|
||||||
|
return Buffer.from(value.type, 'base64').toString('binary');
|
||||||
|
return 'unknown value type: ' + (value as any).type;
|
||||||
|
|
||||||
|
}
|
||||||
527
packages/playwright-core/src/server/bidi/bidiPage.ts
Normal file
527
packages/playwright-core/src/server/bidi/bidiPage.ts
Normal file
|
|
@ -0,0 +1,527 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the 'License');
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an 'AS IS' BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { RegisteredListener } from '../../utils/eventsHelper';
|
||||||
|
import { eventsHelper } from '../../utils/eventsHelper';
|
||||||
|
import { assert } from '../../utils';
|
||||||
|
import type * as accessibility from '../accessibility';
|
||||||
|
import * as dom from '../dom';
|
||||||
|
import * as dialog from '../dialog';
|
||||||
|
import type * as frames from '../frames';
|
||||||
|
import { type InitScript, Page, type PageDelegate } from '../page';
|
||||||
|
import type { Progress } from '../progress';
|
||||||
|
import type * as types from '../types';
|
||||||
|
import type { BidiBrowserContext } from './bidiBrowser';
|
||||||
|
import type { BidiSession } from './bidiConnection';
|
||||||
|
import { RawKeyboardImpl, RawMouseImpl, RawTouchscreenImpl } from './bidiInput';
|
||||||
|
import * as bidi from './third_party/bidiProtocol';
|
||||||
|
import { BidiExecutionContext } from './bidiExecutionContext';
|
||||||
|
import { BidiNetworkManager } from './bidiNetworkManager';
|
||||||
|
import { BrowserContext } from '../browserContext';
|
||||||
|
|
||||||
|
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
|
||||||
|
|
||||||
|
export class BidiPage implements PageDelegate {
|
||||||
|
readonly rawMouse: RawMouseImpl;
|
||||||
|
readonly rawKeyboard: RawKeyboardImpl;
|
||||||
|
readonly rawTouchscreen: RawTouchscreenImpl;
|
||||||
|
readonly _page: Page;
|
||||||
|
private readonly _pagePromise: Promise<Page | Error>;
|
||||||
|
readonly _session: BidiSession;
|
||||||
|
readonly _opener: BidiPage | null;
|
||||||
|
private readonly _realmToContext: Map<string, dom.FrameExecutionContext>;
|
||||||
|
private _sessionListeners: RegisteredListener[] = [];
|
||||||
|
readonly _browserContext: BidiBrowserContext;
|
||||||
|
readonly _networkManager: BidiNetworkManager;
|
||||||
|
_initializedPage: Page | null = null;
|
||||||
|
|
||||||
|
constructor(browserContext: BidiBrowserContext, bidiSession: BidiSession, opener: BidiPage | null) {
|
||||||
|
this._session = bidiSession;
|
||||||
|
this._opener = opener;
|
||||||
|
this.rawKeyboard = new RawKeyboardImpl(bidiSession);
|
||||||
|
this.rawMouse = new RawMouseImpl(bidiSession);
|
||||||
|
this.rawTouchscreen = new RawTouchscreenImpl(bidiSession);
|
||||||
|
this._realmToContext = new Map();
|
||||||
|
this._page = new Page(this, browserContext);
|
||||||
|
this._browserContext = browserContext;
|
||||||
|
this._networkManager = new BidiNetworkManager(this._session, this._page, this._onNavigationResponseStarted.bind(this));
|
||||||
|
this._page.on(Page.Events.FrameDetached, (frame: frames.Frame) => this._removeContextsForFrame(frame, false));
|
||||||
|
this._sessionListeners = [
|
||||||
|
eventsHelper.addEventListener(bidiSession, 'script.realmCreated', this._onRealmCreated.bind(this)),
|
||||||
|
eventsHelper.addEventListener(bidiSession, 'browsingContext.contextDestroyed', this._onBrowsingContextDestroyed.bind(this)),
|
||||||
|
eventsHelper.addEventListener(bidiSession, 'browsingContext.navigationStarted', this._onNavigationStarted.bind(this)),
|
||||||
|
eventsHelper.addEventListener(bidiSession, 'browsingContext.navigationAborted', this._onNavigationAborted.bind(this)),
|
||||||
|
eventsHelper.addEventListener(bidiSession, 'browsingContext.navigationFailed', this._onNavigationFailed.bind(this)),
|
||||||
|
eventsHelper.addEventListener(bidiSession, 'browsingContext.fragmentNavigated', this._onFragmentNavigated.bind(this)),
|
||||||
|
eventsHelper.addEventListener(bidiSession, 'browsingContext.domContentLoaded', this._onDomContentLoaded.bind(this)),
|
||||||
|
eventsHelper.addEventListener(bidiSession, 'browsingContext.load', this._onLoad.bind(this)),
|
||||||
|
eventsHelper.addEventListener(bidiSession, 'browsingContext.userPromptOpened', this._onUserPromptOpened.bind(this)),
|
||||||
|
eventsHelper.addEventListener(bidiSession, 'log.entryAdded', this._onLogEntryAdded.bind(this)),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Initialize main frame.
|
||||||
|
this._pagePromise = this._initialize().finally(async () => {
|
||||||
|
await this._page.initOpener(this._opener);
|
||||||
|
}).then(() => {
|
||||||
|
this._initializedPage = this._page;
|
||||||
|
this._page.reportAsNew();
|
||||||
|
return this._page;
|
||||||
|
}).catch(e => {
|
||||||
|
this._page.reportAsNew(e);
|
||||||
|
return e;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _initialize() {
|
||||||
|
const { contexts } = await this._session.send('browsingContext.getTree', { root: this._session.sessionId });
|
||||||
|
this._handleFrameTree(contexts[0]);
|
||||||
|
await Promise.all([
|
||||||
|
this.updateHttpCredentials(),
|
||||||
|
this.updateRequestInterception(),
|
||||||
|
this._updateViewport(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleFrameTree(frameTree: bidi.BrowsingContext.Info) {
|
||||||
|
this._onFrameAttached(frameTree.context, frameTree.parent || null);
|
||||||
|
if (!frameTree.children)
|
||||||
|
return;
|
||||||
|
|
||||||
|
for (const child of frameTree.children)
|
||||||
|
this._handleFrameTree(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
potentiallyUninitializedPage(): Page {
|
||||||
|
return this._page;
|
||||||
|
}
|
||||||
|
|
||||||
|
didClose() {
|
||||||
|
this._session.dispose();
|
||||||
|
eventsHelper.removeEventListeners(this._sessionListeners);
|
||||||
|
this._page._didClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
async pageOrError(): Promise<Page | Error> {
|
||||||
|
// TODO: Wait for first execution context to be created and maybe about:blank navigated.
|
||||||
|
return this._pagePromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onFrameAttached(frameId: string, parentFrameId: string | null): frames.Frame {
|
||||||
|
return this._page._frameManager.frameAttached(frameId, parentFrameId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _removeContextsForFrame(frame: frames.Frame, notifyFrame: boolean) {
|
||||||
|
for (const [contextId, context] of this._realmToContext) {
|
||||||
|
if (context.frame === frame) {
|
||||||
|
this._realmToContext.delete(contextId);
|
||||||
|
if (notifyFrame)
|
||||||
|
frame._contextDestroyed(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onRealmCreated(realmInfo: bidi.Script.RealmInfo) {
|
||||||
|
if (this._realmToContext.has(realmInfo.realm))
|
||||||
|
return;
|
||||||
|
if (realmInfo.type !== 'window')
|
||||||
|
return;
|
||||||
|
const frame = this._page._frameManager.frame(realmInfo.context);
|
||||||
|
if (!frame)
|
||||||
|
return;
|
||||||
|
const delegate = new BidiExecutionContext(this._session, realmInfo);
|
||||||
|
let worldName: types.World;
|
||||||
|
if (!realmInfo.sandbox) {
|
||||||
|
worldName = 'main';
|
||||||
|
// Force creating utility world every time the main world is created (e.g. due to navigation).
|
||||||
|
this._touchUtilityWorld(realmInfo.context);
|
||||||
|
} else if (realmInfo.sandbox === UTILITY_WORLD_NAME) {
|
||||||
|
worldName = 'utility';
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const context = new dom.FrameExecutionContext(delegate, frame, worldName);
|
||||||
|
(context as any)[contextDelegateSymbol] = delegate;
|
||||||
|
frame._contextCreated(worldName, context);
|
||||||
|
this._realmToContext.set(realmInfo.realm, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _touchUtilityWorld(context: bidi.BrowsingContext.BrowsingContext) {
|
||||||
|
await this._session.sendMayFail('script.evaluate', {
|
||||||
|
expression: '1 + 1',
|
||||||
|
target: {
|
||||||
|
context,
|
||||||
|
sandbox: UTILITY_WORLD_NAME,
|
||||||
|
},
|
||||||
|
serializationOptions: {
|
||||||
|
maxObjectDepth: 10,
|
||||||
|
maxDomDepth: 10,
|
||||||
|
},
|
||||||
|
awaitPromise: true,
|
||||||
|
userActivation: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_onRealmDestroyed(params: bidi.Script.RealmDestroyedParameters): boolean {
|
||||||
|
const context = this._realmToContext.get(params.realm);
|
||||||
|
if (!context)
|
||||||
|
return false;
|
||||||
|
this._realmToContext.delete(params.realm);
|
||||||
|
context.frame._contextDestroyed(context);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: route the message directly to the browser
|
||||||
|
private _onBrowsingContextDestroyed(params: bidi.BrowsingContext.Info) {
|
||||||
|
this._browserContext._browser._onBrowsingContextDestroyed(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onNavigationStarted(params: bidi.BrowsingContext.NavigationInfo) {
|
||||||
|
const frameId = params.context;
|
||||||
|
this._page._frameManager.frameRequestedNavigation(frameId, params.navigation!);
|
||||||
|
|
||||||
|
const url = params.url.toLowerCase();
|
||||||
|
if (url.startsWith('file:') || url.startsWith('data:') || url === 'about:blank') {
|
||||||
|
// Navigation to file urls doesn't emit network events, so we fire 'commit' event right when navigation is started.
|
||||||
|
// Doing it in domcontentload would be too late as we'd clear frame tree.
|
||||||
|
const frame = this._page._frameManager.frame(frameId)!;
|
||||||
|
if (frame)
|
||||||
|
this._page._frameManager.frameCommittedNewDocumentNavigation(frameId, params.url, '', params.navigation!, /* initial */ false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: there is no separate event for committed navigation, so we approximate it with responseStarted.
|
||||||
|
private _onNavigationResponseStarted(params: bidi.Network.ResponseStartedParameters) {
|
||||||
|
const frameId = params.context!;
|
||||||
|
const frame = this._page._frameManager.frame(frameId);
|
||||||
|
assert(frame);
|
||||||
|
this._page._frameManager.frameCommittedNewDocumentNavigation(frameId, params.response.url, '', params.navigation!, /* initial */ false);
|
||||||
|
// if (!initial)
|
||||||
|
// this._firstNonInitialNavigationCommittedFulfill();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onDomContentLoaded(params: bidi.BrowsingContext.NavigationInfo) {
|
||||||
|
const frameId = params.context;
|
||||||
|
this._page._frameManager.frameLifecycleEvent(frameId, 'domcontentloaded');
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onLoad(params: bidi.BrowsingContext.NavigationInfo) {
|
||||||
|
this._page._frameManager.frameLifecycleEvent(params.context, 'load');
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onNavigationAborted(params: bidi.BrowsingContext.NavigationInfo) {
|
||||||
|
this._page._frameManager.frameAbortedNavigation(params.context, 'Navigation aborted', params.navigation || undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onNavigationFailed(params: bidi.BrowsingContext.NavigationInfo) {
|
||||||
|
this._page._frameManager.frameAbortedNavigation(params.context, 'Navigation failed', params.navigation || undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onFragmentNavigated(params: bidi.BrowsingContext.NavigationInfo) {
|
||||||
|
this._page._frameManager.frameCommittedSameDocumentNavigation(params.context, params.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onUserPromptOpened(event: bidi.BrowsingContext.UserPromptOpenedParameters) {
|
||||||
|
this._page.emitOnContext(BrowserContext.Events.Dialog, new dialog.Dialog(
|
||||||
|
this._page,
|
||||||
|
event.type as dialog.DialogType,
|
||||||
|
event.message,
|
||||||
|
async (accept: boolean, userText?: string) => {
|
||||||
|
await this._session.send('browsingContext.handleUserPrompt', { context: event.context, accept, userText });
|
||||||
|
},
|
||||||
|
event.defaultValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onLogEntryAdded(params: bidi.Log.Entry) {
|
||||||
|
if (params.type !== 'console')
|
||||||
|
return;
|
||||||
|
const entry: bidi.Log.ConsoleLogEntry = params as bidi.Log.ConsoleLogEntry;
|
||||||
|
const context = this._realmToContext.get(params.source.realm);
|
||||||
|
if (!context)
|
||||||
|
return;
|
||||||
|
const callFrame = params.stackTrace?.callFrames[0];
|
||||||
|
const location = callFrame ?? { url: '', lineNumber: 1, columnNumber: 1 };
|
||||||
|
this._page._addConsoleMessage(entry.method, entry.args.map(arg => context.createHandle({ objectId: (arg as any).handle, ...arg })), location, params.text || undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
async navigateFrame(frame: frames.Frame, url: string, referrer: string | undefined): Promise<frames.GotoResult> {
|
||||||
|
const { navigation } = await this._session.send('browsingContext.navigate', {
|
||||||
|
context: frame._id,
|
||||||
|
url,
|
||||||
|
});
|
||||||
|
return { newDocumentId: navigation || undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateExtraHTTPHeaders(): Promise<void> {
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateEmulateMedia(): Promise<void> {
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateEmulatedViewportSize(): Promise<void> {
|
||||||
|
await this._updateViewport();
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateUserAgent(): Promise<void> {
|
||||||
|
}
|
||||||
|
|
||||||
|
async bringToFront(): Promise<void> {
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _updateViewport(): Promise<void> {
|
||||||
|
const options = this._browserContext._options;
|
||||||
|
const deviceSize = this._page.emulatedSize();
|
||||||
|
if (deviceSize === null)
|
||||||
|
return;
|
||||||
|
const viewportSize = deviceSize.viewport;
|
||||||
|
await this._session.send('browsingContext.setViewport', {
|
||||||
|
context: this._session.sessionId,
|
||||||
|
viewport: {
|
||||||
|
width: viewportSize.width,
|
||||||
|
height: viewportSize.height,
|
||||||
|
},
|
||||||
|
devicePixelRatio: options.deviceScaleFactor || 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateRequestInterception(): Promise<void> {
|
||||||
|
await this._networkManager.setRequestInterception(this._page.needsRequestInterception());
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateOffline() {
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateHttpCredentials() {
|
||||||
|
await this._networkManager.setCredentials(this._browserContext._options.httpCredentials);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateFileChooserInterception() {
|
||||||
|
}
|
||||||
|
|
||||||
|
async reload(): Promise<void> {
|
||||||
|
await this._session.send('browsingContext.reload', {
|
||||||
|
context: this._session.sessionId,
|
||||||
|
// ignoreCache: true,
|
||||||
|
wait: bidi.BrowsingContext.ReadinessState.Interactive,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
goBack(): Promise<boolean> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
goForward(): Promise<boolean> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async addInitScript(initScript: InitScript): Promise<void> {
|
||||||
|
await this._updateBootstrapScript();
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeNonInternalInitScripts() {
|
||||||
|
await this._updateBootstrapScript();
|
||||||
|
}
|
||||||
|
|
||||||
|
async _updateBootstrapScript(): Promise<void> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async closePage(runBeforeUnload: boolean): Promise<void> {
|
||||||
|
await this._session.send('browsingContext.close', {
|
||||||
|
context: this._session.sessionId,
|
||||||
|
promptUnload: runBeforeUnload,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async setBackgroundColor(color?: { r: number; g: number; b: number; a: number; }): Promise<void> {
|
||||||
|
}
|
||||||
|
|
||||||
|
async takeScreenshot(progress: Progress, format: string, documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined, fitsViewport: boolean, scale: 'css' | 'device'): Promise<Buffer> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getContentFrame(handle: dom.ElementHandle): Promise<frames.Frame | null> {
|
||||||
|
const executionContext = toBidiExecutionContext(handle._context);
|
||||||
|
const contentWindow = await executionContext.rawCallFunction('e => e.contentWindow', { handle: handle._objectId });
|
||||||
|
if (contentWindow.type === 'window') {
|
||||||
|
const frameId = contentWindow.value.context;
|
||||||
|
const result = this._page._frameManager.frame(frameId);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOwnerFrame(handle: dom.ElementHandle): Promise<string | null> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
isElementHandle(remoteObject: bidi.Script.RemoteValue): boolean {
|
||||||
|
return remoteObject.type === 'node';
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBoundingBox(handle: dom.ElementHandle): Promise<types.Rect | null> {
|
||||||
|
const box = await handle.evaluate(element => {
|
||||||
|
if (!(element instanceof Element))
|
||||||
|
return null;
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
|
||||||
|
});
|
||||||
|
if (!box)
|
||||||
|
return null;
|
||||||
|
const position = await this._framePosition(handle._frame);
|
||||||
|
if (!position)
|
||||||
|
return null;
|
||||||
|
box.x += position.x;
|
||||||
|
box.y += position.y;
|
||||||
|
return box;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: move to Frame.
|
||||||
|
private async _framePosition(frame: frames.Frame): Promise<types.Point | null> {
|
||||||
|
if (frame === this._page.mainFrame())
|
||||||
|
return { x: 0, y: 0 };
|
||||||
|
const element = await frame.frameElement();
|
||||||
|
const box = await element.boundingBox();
|
||||||
|
if (!box)
|
||||||
|
return null;
|
||||||
|
const style = await element.evaluateInUtility(([injected, iframe]) => injected.describeIFrameStyle(iframe as Element), {}).catch(e => 'error:notconnected' as const);
|
||||||
|
if (style === 'error:notconnected' || style === 'transformed')
|
||||||
|
return null;
|
||||||
|
// Content box is offset by border and padding widths.
|
||||||
|
box.x += style.left;
|
||||||
|
box.y += style.top;
|
||||||
|
return box;
|
||||||
|
}
|
||||||
|
|
||||||
|
async scrollRectIntoViewIfNeeded(handle: dom.ElementHandle<Element>, rect?: types.Rect): Promise<'error:notvisible' | 'error:notconnected' | 'done'> {
|
||||||
|
return await handle.evaluateInUtility(([injected, node]) => {
|
||||||
|
node.scrollIntoView({
|
||||||
|
block: 'center',
|
||||||
|
inline: 'center',
|
||||||
|
behavior: 'instant',
|
||||||
|
});
|
||||||
|
}, null).then(() => 'done' as const).catch(e => {
|
||||||
|
if (e instanceof Error && e.message.includes('Node is detached from document'))
|
||||||
|
return 'error:notconnected';
|
||||||
|
if (e instanceof Error && e.message.includes('Node does not have a layout object'))
|
||||||
|
return 'error:notvisible';
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async setScreencastOptions(options: { width: number, height: number, quality: number } | null): Promise<void> {
|
||||||
|
}
|
||||||
|
|
||||||
|
rafCountForStablePosition(): number {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getContentQuads(handle: dom.ElementHandle<Element>): Promise<types.Quad[] | null | 'error:notconnected'> {
|
||||||
|
const quads = await handle.evaluateInUtility(([injected, node]) => {
|
||||||
|
if (!node.isConnected)
|
||||||
|
return 'error:notconnected';
|
||||||
|
const rects = node.getClientRects();
|
||||||
|
if (!rects)
|
||||||
|
return null;
|
||||||
|
return [...rects].map(rect => [
|
||||||
|
{ x: rect.left, y: rect.top },
|
||||||
|
{ x: rect.right, y: rect.top },
|
||||||
|
{ x: rect.right, y: rect.bottom },
|
||||||
|
{ x: rect.left, y: rect.bottom },
|
||||||
|
]);
|
||||||
|
}, null);
|
||||||
|
if (!quads || quads === 'error:notconnected')
|
||||||
|
return quads;
|
||||||
|
// TODO: consider transforming quads to support clicks in iframes.
|
||||||
|
const position = await this._framePosition(handle._frame);
|
||||||
|
if (!position)
|
||||||
|
return null;
|
||||||
|
quads.forEach(quad => quad.forEach(point => {
|
||||||
|
point.x += position.x;
|
||||||
|
point.y += position.y;
|
||||||
|
}));
|
||||||
|
return quads as types.Quad[];
|
||||||
|
}
|
||||||
|
|
||||||
|
async setInputFiles(handle: dom.ElementHandle<HTMLInputElement>, files: types.FilePayload[]): Promise<void> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async setInputFilePaths(handle: dom.ElementHandle<HTMLInputElement>, paths: string[]): Promise<void> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async adoptElementHandle<T extends Node>(handle: dom.ElementHandle<T>, to: dom.FrameExecutionContext): Promise<dom.ElementHandle<T>> {
|
||||||
|
const fromContext = toBidiExecutionContext(handle._context);
|
||||||
|
const shared = await fromContext.rawCallFunction('x => x', { handle: handle._objectId });
|
||||||
|
// TODO: store sharedId in the handle.
|
||||||
|
if (!('sharedId' in shared))
|
||||||
|
throw new Error('Element is not a node');
|
||||||
|
const sharedId = shared.sharedId!;
|
||||||
|
const executionContext = toBidiExecutionContext(to);
|
||||||
|
const result = await executionContext.rawCallFunction('x => x', { sharedId });
|
||||||
|
if ('handle' in result)
|
||||||
|
return to.createHandle({ objectId: result.handle!, ...result }) as dom.ElementHandle<T>;
|
||||||
|
throw new Error('Failed to adopt element handle.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAccessibilityTree(needle?: dom.ElementHandle): Promise<{tree: accessibility.AXNode, needle: accessibility.AXNode | null}> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async inputActionEpilogue(): Promise<void> {
|
||||||
|
}
|
||||||
|
|
||||||
|
async resetForReuse(): Promise<void> {
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFrameElement(frame: frames.Frame): Promise<dom.ElementHandle> {
|
||||||
|
const parent = frame.parentFrame();
|
||||||
|
if (!parent)
|
||||||
|
throw new Error('Frame has been detached.');
|
||||||
|
const parentContext = await parent._mainContext();
|
||||||
|
const list = await parentContext.evaluateHandle(() => { return [...document.querySelectorAll('iframe,frame')]; });
|
||||||
|
const length = await list.evaluate(list => list.length);
|
||||||
|
let foundElement = null;
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
const element = await list.evaluateHandle((list, i) => list[i], i);
|
||||||
|
const candidate = await element.contentFrame();
|
||||||
|
if (frame === candidate) {
|
||||||
|
foundElement = element;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
element.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
list.dispose();
|
||||||
|
if (!foundElement)
|
||||||
|
throw new Error('Frame has been detached.');
|
||||||
|
return foundElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldToggleStyleSheetToSyncAnimations(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
useMainWorldForSetContent(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toBidiExecutionContext(executionContext: dom.FrameExecutionContext): BidiExecutionContext {
|
||||||
|
return (executionContext as any)[contextDelegateSymbol] as BidiExecutionContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
const contextDelegateSymbol = Symbol('delegate');
|
||||||
202
packages/playwright-core/src/server/bidi/third_party/LICENSE
vendored
Normal file
202
packages/playwright-core/src/server/bidi/third_party/LICENSE
vendored
Normal file
|
|
@ -0,0 +1,202 @@
|
||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
https://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright 2017 Google Inc.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
176
packages/playwright-core/src/server/bidi/third_party/bidiCommands.d.ts
vendored
Normal file
176
packages/playwright-core/src/server/bidi/third_party/bidiCommands.d.ts
vendored
Normal file
|
|
@ -0,0 +1,176 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2024 Google Inc.
|
||||||
|
* Modifications copyright (c) Microsoft Corporation.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as Bidi from './bidiProtocol';
|
||||||
|
|
||||||
|
export interface Commands {
|
||||||
|
'script.evaluate': {
|
||||||
|
params: Bidi.Script.EvaluateParameters;
|
||||||
|
returnType: Bidi.Script.EvaluateResult;
|
||||||
|
};
|
||||||
|
'script.callFunction': {
|
||||||
|
params: Bidi.Script.CallFunctionParameters;
|
||||||
|
returnType: Bidi.Script.EvaluateResult;
|
||||||
|
};
|
||||||
|
'script.disown': {
|
||||||
|
params: Bidi.Script.DisownParameters;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
'script.addPreloadScript': {
|
||||||
|
params: Bidi.Script.AddPreloadScriptParameters;
|
||||||
|
returnType: Bidi.Script.AddPreloadScriptResult;
|
||||||
|
};
|
||||||
|
'script.removePreloadScript': {
|
||||||
|
params: Bidi.Script.RemovePreloadScriptParameters;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
|
||||||
|
'browser.close': {
|
||||||
|
params: Bidi.EmptyParams;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
|
||||||
|
'browser.createUserContext': {
|
||||||
|
params: Bidi.EmptyParams;
|
||||||
|
returnType: Bidi.Browser.CreateUserContextResult;
|
||||||
|
};
|
||||||
|
'browser.getUserContexts': {
|
||||||
|
params: Bidi.EmptyParams;
|
||||||
|
returnType: Bidi.Browser.GetUserContextsResult;
|
||||||
|
};
|
||||||
|
'browser.removeUserContext': {
|
||||||
|
params: {
|
||||||
|
userContext: Bidi.Browser.UserContext;
|
||||||
|
};
|
||||||
|
returnType: Bidi.Browser.RemoveUserContext;
|
||||||
|
};
|
||||||
|
|
||||||
|
'browsingContext.activate': {
|
||||||
|
params: Bidi.BrowsingContext.ActivateParameters;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
'browsingContext.create': {
|
||||||
|
params: Bidi.BrowsingContext.CreateParameters;
|
||||||
|
returnType: Bidi.BrowsingContext.CreateResult;
|
||||||
|
};
|
||||||
|
'browsingContext.close': {
|
||||||
|
params: Bidi.BrowsingContext.CloseParameters;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
'browsingContext.getTree': {
|
||||||
|
params: Bidi.BrowsingContext.GetTreeParameters;
|
||||||
|
returnType: Bidi.BrowsingContext.GetTreeResult;
|
||||||
|
};
|
||||||
|
'browsingContext.locateNodes': {
|
||||||
|
params: Bidi.BrowsingContext.LocateNodesParameters;
|
||||||
|
returnType: Bidi.BrowsingContext.LocateNodesResult;
|
||||||
|
};
|
||||||
|
'browsingContext.navigate': {
|
||||||
|
params: Bidi.BrowsingContext.NavigateParameters;
|
||||||
|
returnType: Bidi.BrowsingContext.NavigateResult;
|
||||||
|
};
|
||||||
|
'browsingContext.reload': {
|
||||||
|
params: Bidi.BrowsingContext.ReloadParameters;
|
||||||
|
returnType: Bidi.BrowsingContext.NavigateResult;
|
||||||
|
};
|
||||||
|
'browsingContext.print': {
|
||||||
|
params: Bidi.BrowsingContext.PrintParameters;
|
||||||
|
returnType: Bidi.BrowsingContext.PrintResult;
|
||||||
|
};
|
||||||
|
'browsingContext.captureScreenshot': {
|
||||||
|
params: Bidi.BrowsingContext.CaptureScreenshotParameters;
|
||||||
|
returnType: Bidi.BrowsingContext.CaptureScreenshotResult;
|
||||||
|
};
|
||||||
|
'browsingContext.handleUserPrompt': {
|
||||||
|
params: Bidi.BrowsingContext.HandleUserPromptParameters;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
'browsingContext.setViewport': {
|
||||||
|
params: Bidi.BrowsingContext.SetViewportParameters;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
'browsingContext.traverseHistory': {
|
||||||
|
params: Bidi.BrowsingContext.TraverseHistoryParameters;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
|
||||||
|
'input.performActions': {
|
||||||
|
params: Bidi.Input.PerformActionsParameters;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
'input.releaseActions': {
|
||||||
|
params: Bidi.Input.ReleaseActionsParameters;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
'input.setFiles': {
|
||||||
|
params: Bidi.Input.SetFilesParameters;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
|
||||||
|
'session.end': {
|
||||||
|
params: Bidi.EmptyParams;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
'session.new': {
|
||||||
|
params: Bidi.Session.NewParameters;
|
||||||
|
returnType: Bidi.Session.NewResult;
|
||||||
|
};
|
||||||
|
'session.status': {
|
||||||
|
params: object;
|
||||||
|
returnType: Bidi.Session.StatusResult;
|
||||||
|
};
|
||||||
|
'session.subscribe': {
|
||||||
|
params: Bidi.Session.SubscriptionRequest;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
'session.unsubscribe': {
|
||||||
|
params: Bidi.Session.SubscriptionRequest;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
|
||||||
|
'storage.deleteCookies': {
|
||||||
|
params: Bidi.Storage.DeleteCookiesParameters;
|
||||||
|
returnType: Bidi.Storage.DeleteCookiesResult;
|
||||||
|
};
|
||||||
|
'storage.getCookies': {
|
||||||
|
params: Bidi.Storage.GetCookiesParameters;
|
||||||
|
returnType: Bidi.Storage.GetCookiesResult;
|
||||||
|
};
|
||||||
|
'network.setCacheBehavior': {
|
||||||
|
params: Bidi.Network.SetCacheBehaviorParameters;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
'storage.setCookie': {
|
||||||
|
params: Bidi.Storage.SetCookieParameters;
|
||||||
|
returnType: Bidi.Storage.SetCookieParameters;
|
||||||
|
};
|
||||||
|
|
||||||
|
'network.addIntercept': {
|
||||||
|
params: Bidi.Network.AddInterceptParameters;
|
||||||
|
returnType: Bidi.Network.AddInterceptResult;
|
||||||
|
};
|
||||||
|
'network.removeIntercept': {
|
||||||
|
params: Bidi.Network.RemoveInterceptParameters;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
'network.continueRequest': {
|
||||||
|
params: Bidi.Network.ContinueRequestParameters;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
'network.continueWithAuth': {
|
||||||
|
params: Bidi.Network.ContinueWithAuthParameters;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
'network.failRequest': {
|
||||||
|
params: Bidi.Network.FailRequestParameters;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
'network.provideResponse': {
|
||||||
|
params: Bidi.Network.ProvideResponseParameters;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
}
|
||||||
91
packages/playwright-core/src/server/bidi/third_party/bidiDeserializer.ts
vendored
Normal file
91
packages/playwright-core/src/server/bidi/third_party/bidiDeserializer.ts
vendored
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2024 Google Inc.
|
||||||
|
* Modifications copyright (c) Microsoft Corporation.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
import type * as Bidi from './bidiProtocol';
|
||||||
|
|
||||||
|
/* eslint-disable object-curly-spacing */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export class BidiDeserializer {
|
||||||
|
static deserialize(result: Bidi.Script.RemoteValue): any {
|
||||||
|
if (!result)
|
||||||
|
return undefined;
|
||||||
|
|
||||||
|
switch (result.type) {
|
||||||
|
case 'array':
|
||||||
|
return result.value?.map(value => {
|
||||||
|
return BidiDeserializer.deserialize(value);
|
||||||
|
});
|
||||||
|
case 'set':
|
||||||
|
return result.value?.reduce((acc: Set<unknown>, value) => {
|
||||||
|
return acc.add(BidiDeserializer.deserialize(value));
|
||||||
|
}, new Set());
|
||||||
|
case 'object':
|
||||||
|
return result.value?.reduce((acc: Record<any, unknown>, tuple) => {
|
||||||
|
const {key, value} = BidiDeserializer._deserializeTuple(tuple);
|
||||||
|
acc[key as any] = value;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
case 'map':
|
||||||
|
return result.value?.reduce((acc: Map<unknown, unknown>, tuple) => {
|
||||||
|
const {key, value} = BidiDeserializer._deserializeTuple(tuple);
|
||||||
|
return acc.set(key, value);
|
||||||
|
}, new Map());
|
||||||
|
case 'promise':
|
||||||
|
return {};
|
||||||
|
case 'regexp':
|
||||||
|
return new RegExp(result.value.pattern, result.value.flags);
|
||||||
|
case 'date':
|
||||||
|
return new Date(result.value);
|
||||||
|
case 'undefined':
|
||||||
|
return undefined;
|
||||||
|
case 'null':
|
||||||
|
return null;
|
||||||
|
case 'number':
|
||||||
|
return BidiDeserializer._deserializeNumber(result.value);
|
||||||
|
case 'bigint':
|
||||||
|
return BigInt(result.value);
|
||||||
|
case 'boolean':
|
||||||
|
return Boolean(result.value);
|
||||||
|
case 'string':
|
||||||
|
return result.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Deserialization of type ${result.type} not supported.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
static _deserializeNumber(value: Bidi.Script.SpecialNumber | number): number {
|
||||||
|
switch (value) {
|
||||||
|
case '-0':
|
||||||
|
return -0;
|
||||||
|
case 'NaN':
|
||||||
|
return NaN;
|
||||||
|
case 'Infinity':
|
||||||
|
return Infinity;
|
||||||
|
case '-Infinity':
|
||||||
|
return -Infinity;
|
||||||
|
default:
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static _deserializeTuple([serializedKey, serializedValue]: [
|
||||||
|
Bidi.Script.RemoteValue | string,
|
||||||
|
Bidi.Script.RemoteValue,
|
||||||
|
]): {key: unknown; value: unknown} {
|
||||||
|
const key =
|
||||||
|
typeof serializedKey === 'string'
|
||||||
|
? serializedKey
|
||||||
|
: BidiDeserializer.deserialize(serializedKey);
|
||||||
|
const value = BidiDeserializer.deserialize(serializedValue);
|
||||||
|
|
||||||
|
return {key, value};
|
||||||
|
}
|
||||||
|
}
|
||||||
231
packages/playwright-core/src/server/bidi/third_party/bidiKeyboard.ts
vendored
Normal file
231
packages/playwright-core/src/server/bidi/third_party/bidiKeyboard.ts
vendored
Normal file
|
|
@ -0,0 +1,231 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2024 Google Inc.
|
||||||
|
* Modifications copyright (c) Microsoft Corporation.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable curly */
|
||||||
|
|
||||||
|
export const getBidiKeyValue = (key: string) => {
|
||||||
|
switch (key) {
|
||||||
|
case '\r':
|
||||||
|
case '\n':
|
||||||
|
key = 'Enter';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Measures the number of code points rather than UTF-16 code units.
|
||||||
|
if ([...key].length === 1) {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
switch (key) {
|
||||||
|
case 'Cancel':
|
||||||
|
return '\uE001';
|
||||||
|
case 'Help':
|
||||||
|
return '\uE002';
|
||||||
|
case 'Backspace':
|
||||||
|
return '\uE003';
|
||||||
|
case 'Tab':
|
||||||
|
return '\uE004';
|
||||||
|
case 'Clear':
|
||||||
|
return '\uE005';
|
||||||
|
case 'Enter':
|
||||||
|
return '\uE007';
|
||||||
|
case 'Shift':
|
||||||
|
case 'ShiftLeft':
|
||||||
|
return '\uE008';
|
||||||
|
case 'Control':
|
||||||
|
case 'ControlLeft':
|
||||||
|
return '\uE009';
|
||||||
|
case 'Alt':
|
||||||
|
case 'AltLeft':
|
||||||
|
return '\uE00A';
|
||||||
|
case 'Pause':
|
||||||
|
return '\uE00B';
|
||||||
|
case 'Escape':
|
||||||
|
return '\uE00C';
|
||||||
|
case 'PageUp':
|
||||||
|
return '\uE00E';
|
||||||
|
case 'PageDown':
|
||||||
|
return '\uE00F';
|
||||||
|
case 'End':
|
||||||
|
return '\uE010';
|
||||||
|
case 'Home':
|
||||||
|
return '\uE011';
|
||||||
|
case 'ArrowLeft':
|
||||||
|
return '\uE012';
|
||||||
|
case 'ArrowUp':
|
||||||
|
return '\uE013';
|
||||||
|
case 'ArrowRight':
|
||||||
|
return '\uE014';
|
||||||
|
case 'ArrowDown':
|
||||||
|
return '\uE015';
|
||||||
|
case 'Insert':
|
||||||
|
return '\uE016';
|
||||||
|
case 'Delete':
|
||||||
|
return '\uE017';
|
||||||
|
case 'NumpadEqual':
|
||||||
|
return '\uE019';
|
||||||
|
case 'Numpad0':
|
||||||
|
return '\uE01A';
|
||||||
|
case 'Numpad1':
|
||||||
|
return '\uE01B';
|
||||||
|
case 'Numpad2':
|
||||||
|
return '\uE01C';
|
||||||
|
case 'Numpad3':
|
||||||
|
return '\uE01D';
|
||||||
|
case 'Numpad4':
|
||||||
|
return '\uE01E';
|
||||||
|
case 'Numpad5':
|
||||||
|
return '\uE01F';
|
||||||
|
case 'Numpad6':
|
||||||
|
return '\uE020';
|
||||||
|
case 'Numpad7':
|
||||||
|
return '\uE021';
|
||||||
|
case 'Numpad8':
|
||||||
|
return '\uE022';
|
||||||
|
case 'Numpad9':
|
||||||
|
return '\uE023';
|
||||||
|
case 'NumpadMultiply':
|
||||||
|
return '\uE024';
|
||||||
|
case 'NumpadAdd':
|
||||||
|
return '\uE025';
|
||||||
|
case 'NumpadSubtract':
|
||||||
|
return '\uE027';
|
||||||
|
case 'NumpadDecimal':
|
||||||
|
return '\uE028';
|
||||||
|
case 'NumpadDivide':
|
||||||
|
return '\uE029';
|
||||||
|
case 'F1':
|
||||||
|
return '\uE031';
|
||||||
|
case 'F2':
|
||||||
|
return '\uE032';
|
||||||
|
case 'F3':
|
||||||
|
return '\uE033';
|
||||||
|
case 'F4':
|
||||||
|
return '\uE034';
|
||||||
|
case 'F5':
|
||||||
|
return '\uE035';
|
||||||
|
case 'F6':
|
||||||
|
return '\uE036';
|
||||||
|
case 'F7':
|
||||||
|
return '\uE037';
|
||||||
|
case 'F8':
|
||||||
|
return '\uE038';
|
||||||
|
case 'F9':
|
||||||
|
return '\uE039';
|
||||||
|
case 'F10':
|
||||||
|
return '\uE03A';
|
||||||
|
case 'F11':
|
||||||
|
return '\uE03B';
|
||||||
|
case 'F12':
|
||||||
|
return '\uE03C';
|
||||||
|
case 'Meta':
|
||||||
|
case 'MetaLeft':
|
||||||
|
return '\uE03D';
|
||||||
|
case 'ShiftRight':
|
||||||
|
return '\uE050';
|
||||||
|
case 'ControlRight':
|
||||||
|
return '\uE051';
|
||||||
|
case 'AltRight':
|
||||||
|
return '\uE052';
|
||||||
|
case 'MetaRight':
|
||||||
|
return '\uE053';
|
||||||
|
case 'Digit0':
|
||||||
|
return '0';
|
||||||
|
case 'Digit1':
|
||||||
|
return '1';
|
||||||
|
case 'Digit2':
|
||||||
|
return '2';
|
||||||
|
case 'Digit3':
|
||||||
|
return '3';
|
||||||
|
case 'Digit4':
|
||||||
|
return '4';
|
||||||
|
case 'Digit5':
|
||||||
|
return '5';
|
||||||
|
case 'Digit6':
|
||||||
|
return '6';
|
||||||
|
case 'Digit7':
|
||||||
|
return '7';
|
||||||
|
case 'Digit8':
|
||||||
|
return '8';
|
||||||
|
case 'Digit9':
|
||||||
|
return '9';
|
||||||
|
case 'KeyA':
|
||||||
|
return 'a';
|
||||||
|
case 'KeyB':
|
||||||
|
return 'b';
|
||||||
|
case 'KeyC':
|
||||||
|
return 'c';
|
||||||
|
case 'KeyD':
|
||||||
|
return 'd';
|
||||||
|
case 'KeyE':
|
||||||
|
return 'e';
|
||||||
|
case 'KeyF':
|
||||||
|
return 'f';
|
||||||
|
case 'KeyG':
|
||||||
|
return 'g';
|
||||||
|
case 'KeyH':
|
||||||
|
return 'h';
|
||||||
|
case 'KeyI':
|
||||||
|
return 'i';
|
||||||
|
case 'KeyJ':
|
||||||
|
return 'j';
|
||||||
|
case 'KeyK':
|
||||||
|
return 'k';
|
||||||
|
case 'KeyL':
|
||||||
|
return 'l';
|
||||||
|
case 'KeyM':
|
||||||
|
return 'm';
|
||||||
|
case 'KeyN':
|
||||||
|
return 'n';
|
||||||
|
case 'KeyO':
|
||||||
|
return 'o';
|
||||||
|
case 'KeyP':
|
||||||
|
return 'p';
|
||||||
|
case 'KeyQ':
|
||||||
|
return 'q';
|
||||||
|
case 'KeyR':
|
||||||
|
return 'r';
|
||||||
|
case 'KeyS':
|
||||||
|
return 's';
|
||||||
|
case 'KeyT':
|
||||||
|
return 't';
|
||||||
|
case 'KeyU':
|
||||||
|
return 'u';
|
||||||
|
case 'KeyV':
|
||||||
|
return 'v';
|
||||||
|
case 'KeyW':
|
||||||
|
return 'w';
|
||||||
|
case 'KeyX':
|
||||||
|
return 'x';
|
||||||
|
case 'KeyY':
|
||||||
|
return 'y';
|
||||||
|
case 'KeyZ':
|
||||||
|
return 'z';
|
||||||
|
case 'Semicolon':
|
||||||
|
return ';';
|
||||||
|
case 'Equal':
|
||||||
|
return '=';
|
||||||
|
case 'Comma':
|
||||||
|
return ',';
|
||||||
|
case 'Minus':
|
||||||
|
return '-';
|
||||||
|
case 'Period':
|
||||||
|
return '.';
|
||||||
|
case 'Slash':
|
||||||
|
return '/';
|
||||||
|
case 'Backquote':
|
||||||
|
return '`';
|
||||||
|
case 'BracketLeft':
|
||||||
|
return '[';
|
||||||
|
case 'Backslash':
|
||||||
|
return '\\';
|
||||||
|
case 'BracketRight':
|
||||||
|
return ']';
|
||||||
|
case 'Quote':
|
||||||
|
return '"';
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown key: "${key}"`);
|
||||||
|
}
|
||||||
|
};
|
||||||
2204
packages/playwright-core/src/server/bidi/third_party/bidiProtocol.ts
vendored
Normal file
2204
packages/playwright-core/src/server/bidi/third_party/bidiProtocol.ts
vendored
Normal file
File diff suppressed because it is too large
Load diff
148
packages/playwright-core/src/server/bidi/third_party/bidiSerializer.ts
vendored
Normal file
148
packages/playwright-core/src/server/bidi/third_party/bidiSerializer.ts
vendored
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2024 Google Inc.
|
||||||
|
* Modifications copyright (c) Microsoft Corporation.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type * as Bidi from './bidiProtocol';
|
||||||
|
|
||||||
|
/* eslint-disable curly, indent */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class UnserializableError extends Error {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export class BidiSerializer {
|
||||||
|
static serialize(arg: unknown): Bidi.Script.LocalValue {
|
||||||
|
switch (typeof arg) {
|
||||||
|
case 'symbol':
|
||||||
|
case 'function':
|
||||||
|
throw new UnserializableError(`Unable to serializable ${typeof arg}`);
|
||||||
|
case 'object':
|
||||||
|
return BidiSerializer._serializeObject(arg);
|
||||||
|
|
||||||
|
case 'undefined':
|
||||||
|
return {
|
||||||
|
type: 'undefined',
|
||||||
|
};
|
||||||
|
case 'number':
|
||||||
|
return BidiSerializer._serializeNumber(arg);
|
||||||
|
case 'bigint':
|
||||||
|
return {
|
||||||
|
type: 'bigint',
|
||||||
|
value: arg.toString(),
|
||||||
|
};
|
||||||
|
case 'string':
|
||||||
|
return {
|
||||||
|
type: 'string',
|
||||||
|
value: arg,
|
||||||
|
};
|
||||||
|
case 'boolean':
|
||||||
|
return {
|
||||||
|
type: 'boolean',
|
||||||
|
value: arg,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static _serializeNumber(arg: number): Bidi.Script.LocalValue {
|
||||||
|
let value: Bidi.Script.SpecialNumber | number;
|
||||||
|
if (Object.is(arg, -0)) {
|
||||||
|
value = '-0';
|
||||||
|
} else if (Object.is(arg, Infinity)) {
|
||||||
|
value = 'Infinity';
|
||||||
|
} else if (Object.is(arg, -Infinity)) {
|
||||||
|
value = '-Infinity';
|
||||||
|
} else if (Object.is(arg, NaN)) {
|
||||||
|
value = 'NaN';
|
||||||
|
} else {
|
||||||
|
value = arg;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: 'number',
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static _serializeObject(arg: object | null): Bidi.Script.LocalValue {
|
||||||
|
if (arg === null) {
|
||||||
|
return {
|
||||||
|
type: 'null',
|
||||||
|
};
|
||||||
|
} else if (Array.isArray(arg)) {
|
||||||
|
const parsedArray = arg.map(subArg => {
|
||||||
|
return BidiSerializer.serialize(subArg);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'array',
|
||||||
|
value: parsedArray,
|
||||||
|
};
|
||||||
|
} else if (isPlainObject(arg)) {
|
||||||
|
try {
|
||||||
|
JSON.stringify(arg);
|
||||||
|
} catch (error) {
|
||||||
|
if (
|
||||||
|
error instanceof TypeError &&
|
||||||
|
error.message.startsWith('Converting circular structure to JSON')
|
||||||
|
) {
|
||||||
|
error.message += ' Recursive objects are not allowed.';
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedObject: Bidi.Script.MappingLocalValue = [];
|
||||||
|
for (const key in arg) {
|
||||||
|
parsedObject.push([BidiSerializer.serialize(key), BidiSerializer.serialize(arg[key])]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'object',
|
||||||
|
value: parsedObject,
|
||||||
|
};
|
||||||
|
} else if (isRegExp(arg)) {
|
||||||
|
return {
|
||||||
|
type: 'regexp',
|
||||||
|
value: {
|
||||||
|
pattern: arg.source,
|
||||||
|
flags: arg.flags,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else if (isDate(arg)) {
|
||||||
|
return {
|
||||||
|
type: 'date',
|
||||||
|
value: arg.toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new UnserializableError(
|
||||||
|
'Custom object serialization not possible. Use plain objects instead.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export const isPlainObject = (obj: unknown): obj is Record<any, unknown> => {
|
||||||
|
return typeof obj === 'object' && obj?.constructor === Object;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export const isRegExp = (obj: unknown): obj is RegExp => {
|
||||||
|
return typeof obj === 'object' && obj?.constructor === RegExp;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export const isDate = (obj: unknown): obj is Date => {
|
||||||
|
return typeof obj === 'object' && obj?.constructor === Date;
|
||||||
|
};
|
||||||
|
|
@ -52,6 +52,7 @@ export interface BrowserReadyState {
|
||||||
|
|
||||||
export abstract class BrowserType extends SdkObject {
|
export abstract class BrowserType extends SdkObject {
|
||||||
private _name: BrowserName;
|
private _name: BrowserName;
|
||||||
|
_useBidi: boolean = false;
|
||||||
|
|
||||||
constructor(parent: SdkObject, browserName: BrowserName) {
|
constructor(parent: SdkObject, browserName: BrowserName) {
|
||||||
super(parent, 'browser-type');
|
super(parent, 'browser-type');
|
||||||
|
|
@ -69,6 +70,8 @@ export abstract class BrowserType extends SdkObject {
|
||||||
|
|
||||||
async launch(metadata: CallMetadata, options: types.LaunchOptions, protocolLogger?: types.ProtocolLogger): Promise<Browser> {
|
async launch(metadata: CallMetadata, options: types.LaunchOptions, protocolLogger?: types.ProtocolLogger): Promise<Browser> {
|
||||||
options = this._validateLaunchOptions(options);
|
options = this._validateLaunchOptions(options);
|
||||||
|
if (this._useBidi)
|
||||||
|
options.useWebSocket = true;
|
||||||
const controller = new ProgressController(metadata, this);
|
const controller = new ProgressController(metadata, this);
|
||||||
controller.setLogName('browser');
|
controller.setLogName('browser');
|
||||||
const browser = await controller.run(progress => {
|
const browser = await controller.run(progress => {
|
||||||
|
|
@ -82,6 +85,8 @@ export abstract class BrowserType extends SdkObject {
|
||||||
|
|
||||||
async launchPersistentContext(metadata: CallMetadata, userDataDir: string, options: channels.BrowserTypeLaunchPersistentContextOptions & { useWebSocket?: boolean }): Promise<BrowserContext> {
|
async launchPersistentContext(metadata: CallMetadata, userDataDir: string, options: channels.BrowserTypeLaunchPersistentContextOptions & { useWebSocket?: boolean }): Promise<BrowserContext> {
|
||||||
options = this._validateLaunchOptions(options);
|
options = this._validateLaunchOptions(options);
|
||||||
|
if (this._useBidi)
|
||||||
|
options.useWebSocket = true;
|
||||||
const controller = new ProgressController(metadata, this);
|
const controller = new ProgressController(metadata, this);
|
||||||
const persistent: channels.BrowserNewContextParams = { ...options };
|
const persistent: channels.BrowserNewContextParams = { ...options };
|
||||||
controller.setLogName('browser');
|
controller.setLogName('browser');
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ export class PlaywrightDispatcher extends Dispatcher<Playwright, channels.Playwr
|
||||||
const prelaunchedAndroidDeviceDispatcher = prelaunchedAndroidDevice ? new AndroidDeviceDispatcher(android, prelaunchedAndroidDevice) : undefined;
|
const prelaunchedAndroidDeviceDispatcher = prelaunchedAndroidDevice ? new AndroidDeviceDispatcher(android, prelaunchedAndroidDevice) : undefined;
|
||||||
super(scope, playwright, 'Playwright', {
|
super(scope, playwright, 'Playwright', {
|
||||||
chromium: new BrowserTypeDispatcher(scope, playwright.chromium),
|
chromium: new BrowserTypeDispatcher(scope, playwright.chromium),
|
||||||
|
bidi: new BrowserTypeDispatcher(scope, playwright.bidi),
|
||||||
firefox: new BrowserTypeDispatcher(scope, playwright.firefox),
|
firefox: new BrowserTypeDispatcher(scope, playwright.firefox),
|
||||||
webkit: new BrowserTypeDispatcher(scope, playwright.webkit),
|
webkit: new BrowserTypeDispatcher(scope, playwright.webkit),
|
||||||
android,
|
android,
|
||||||
|
|
|
||||||
|
|
@ -233,7 +233,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
this._page._timeoutSettings.timeout(options));
|
this._page._timeoutSettings.timeout(options));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _clickablePoint(): Promise<types.Point | 'error:notvisible' | 'error:notinviewport'> {
|
private async _clickablePoint(): Promise<types.Point | 'error:notvisible' | 'error:notinviewport' | 'error:notconnected'> {
|
||||||
const intersectQuadWithViewport = (quad: types.Quad): types.Quad => {
|
const intersectQuadWithViewport = (quad: types.Quad): types.Quad => {
|
||||||
return quad.map(point => ({
|
return quad.map(point => ({
|
||||||
x: Math.min(Math.max(point.x, 0), metrics.width),
|
x: Math.min(Math.max(point.x, 0), metrics.width),
|
||||||
|
|
@ -257,6 +257,8 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
this._page._delegate.getContentQuads(this),
|
this._page._delegate.getContentQuads(this),
|
||||||
this._page.mainFrame()._utilityContext().then(utility => utility.evaluate(() => ({ width: innerWidth, height: innerHeight }))),
|
this._page.mainFrame()._utilityContext().then(utility => utility.evaluate(() => ({ width: innerWidth, height: innerHeight }))),
|
||||||
] as const);
|
] as const);
|
||||||
|
if (quads === 'error:notconnected')
|
||||||
|
return quads;
|
||||||
if (!quads || !quads.length)
|
if (!quads || !quads.length)
|
||||||
return 'error:notvisible';
|
return 'error:notvisible';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -900,7 +900,7 @@ export class Frame extends SdkObject {
|
||||||
const waitUntil = options.waitUntil === undefined ? 'load' : options.waitUntil;
|
const waitUntil = options.waitUntil === undefined ? 'load' : options.waitUntil;
|
||||||
progress.log(`setting frame content, waiting until "${waitUntil}"`);
|
progress.log(`setting frame content, waiting until "${waitUntil}"`);
|
||||||
const tag = `--playwright--set--content--${this._id}--${++this._setContentCounter}--`;
|
const tag = `--playwright--set--content--${this._id}--${++this._setContentCounter}--`;
|
||||||
const context = await this._utilityContext();
|
const context = this._page._delegate.useMainWorldForSetContent?.() ? await this._mainContext() : await this._utilityContext();
|
||||||
const lifecyclePromise = new Promise((resolve, reject) => {
|
const lifecyclePromise = new Promise((resolve, reject) => {
|
||||||
this._page._frameManager._consoleMessageTags.set(tag, () => {
|
this._page._frameManager._consoleMessageTags.set(tag, () => {
|
||||||
// Clear lifecycle right after document.open() - see 'tag' below.
|
// Clear lifecycle right after document.open() - see 'tag' below.
|
||||||
|
|
|
||||||
|
|
@ -162,6 +162,7 @@ export interface RawMouse {
|
||||||
move(x: number, y: number, button: types.MouseButton | 'none', buttons: Set<types.MouseButton>, modifiers: Set<types.KeyboardModifier>, forClick: boolean): Promise<void>;
|
move(x: number, y: number, button: types.MouseButton | 'none', buttons: Set<types.MouseButton>, modifiers: Set<types.KeyboardModifier>, forClick: boolean): Promise<void>;
|
||||||
down(x: number, y: number, button: types.MouseButton, buttons: Set<types.MouseButton>, modifiers: Set<types.KeyboardModifier>, clickCount: number): Promise<void>;
|
down(x: number, y: number, button: types.MouseButton, buttons: Set<types.MouseButton>, modifiers: Set<types.KeyboardModifier>, clickCount: number): Promise<void>;
|
||||||
up(x: number, y: number, button: types.MouseButton, buttons: Set<types.MouseButton>, modifiers: Set<types.KeyboardModifier>, clickCount: number): Promise<void>;
|
up(x: number, y: number, button: types.MouseButton, buttons: Set<types.MouseButton>, modifiers: Set<types.KeyboardModifier>, clickCount: number): Promise<void>;
|
||||||
|
click?(x: number, y: number, options: { delay?: number, button?: types.MouseButton, clickCount?: number }): Promise<void>;
|
||||||
wheel(x: number, y: number, buttons: Set<types.MouseButton>, modifiers: Set<types.KeyboardModifier>, deltaX: number, deltaY: number): Promise<void>;
|
wheel(x: number, y: number, buttons: Set<types.MouseButton>, modifiers: Set<types.KeyboardModifier>, deltaX: number, deltaY: number): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -216,6 +217,8 @@ export class Mouse {
|
||||||
async click(x: number, y: number, options: { delay?: number, button?: types.MouseButton, clickCount?: number } = {}, metadata?: CallMetadata) {
|
async click(x: number, y: number, options: { delay?: number, button?: types.MouseButton, clickCount?: number } = {}, metadata?: CallMetadata) {
|
||||||
if (metadata)
|
if (metadata)
|
||||||
metadata.point = { x, y };
|
metadata.point = { x, y };
|
||||||
|
if (this._raw.click)
|
||||||
|
return await this._raw.click(x, y, options);
|
||||||
const { delay = null, clickCount = 1 } = options;
|
const { delay = null, clickCount = 1 } = options;
|
||||||
if (delay) {
|
if (delay) {
|
||||||
this.move(x, y, { forClick: true });
|
this.move(x, y, { forClick: true });
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,7 @@ export class Request extends SdkObject {
|
||||||
private _waitForResponsePromise = new ManualPromise<Response | null>();
|
private _waitForResponsePromise = new ManualPromise<Response | null>();
|
||||||
_responseEndTiming = -1;
|
_responseEndTiming = -1;
|
||||||
private _overrides: NormalizedContinueOverrides | undefined;
|
private _overrides: NormalizedContinueOverrides | undefined;
|
||||||
|
private _bodySize: number | undefined;
|
||||||
|
|
||||||
constructor(context: contexts.BrowserContext, frame: frames.Frame | null, serviceWorker: pages.Worker | null, redirectedFrom: Request | null, documentId: string | undefined,
|
constructor(context: contexts.BrowserContext, frame: frames.Frame | null, serviceWorker: pages.Worker | null, redirectedFrom: Request | null, documentId: string | undefined,
|
||||||
url: string, resourceType: string, method: string, postData: Buffer | null, headers: HeadersArray) {
|
url: string, resourceType: string, method: string, postData: Buffer | null, headers: HeadersArray) {
|
||||||
|
|
@ -223,8 +224,13 @@ export class Request extends SdkObject {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(bidi): remove once post body is available.
|
||||||
|
_setBodySize(size: number) {
|
||||||
|
this._bodySize = size;
|
||||||
|
}
|
||||||
|
|
||||||
bodySize(): number {
|
bodySize(): number {
|
||||||
return this.postDataBuffer()?.length || 0;
|
return this._bodySize || this.postDataBuffer()?.length || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
async requestHeadersSize(): Promise<number> {
|
async requestHeadersSize(): Promise<number> {
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ export interface PageDelegate {
|
||||||
adoptElementHandle<T extends Node>(handle: dom.ElementHandle<T>, to: dom.FrameExecutionContext): Promise<dom.ElementHandle<T>>;
|
adoptElementHandle<T extends Node>(handle: dom.ElementHandle<T>, to: dom.FrameExecutionContext): Promise<dom.ElementHandle<T>>;
|
||||||
getContentFrame(handle: dom.ElementHandle): Promise<frames.Frame | null>; // Only called for frame owner elements.
|
getContentFrame(handle: dom.ElementHandle): Promise<frames.Frame | null>; // Only called for frame owner elements.
|
||||||
getOwnerFrame(handle: dom.ElementHandle): Promise<string | null>; // Returns frameId.
|
getOwnerFrame(handle: dom.ElementHandle): Promise<string | null>; // Returns frameId.
|
||||||
getContentQuads(handle: dom.ElementHandle): Promise<types.Quad[] | null>;
|
getContentQuads(handle: dom.ElementHandle): Promise<types.Quad[] | null | 'error:notconnected'>;
|
||||||
setInputFiles(handle: dom.ElementHandle<HTMLInputElement>, files: types.FilePayload[]): Promise<void>;
|
setInputFiles(handle: dom.ElementHandle<HTMLInputElement>, files: types.FilePayload[]): Promise<void>;
|
||||||
setInputFilePaths(handle: dom.ElementHandle<HTMLInputElement>, files: string[]): Promise<void>;
|
setInputFilePaths(handle: dom.ElementHandle<HTMLInputElement>, files: string[]): Promise<void>;
|
||||||
getBoundingBox(handle: dom.ElementHandle): Promise<types.Rect | null>;
|
getBoundingBox(handle: dom.ElementHandle): Promise<types.Rect | null>;
|
||||||
|
|
@ -98,6 +98,8 @@ export interface PageDelegate {
|
||||||
resetForReuse(): Promise<void>;
|
resetForReuse(): Promise<void>;
|
||||||
// WebKit hack.
|
// WebKit hack.
|
||||||
shouldToggleStyleSheetToSyncAnimations(): boolean;
|
shouldToggleStyleSheetToSyncAnimations(): boolean;
|
||||||
|
// Bidi throws on attempt to document.open() in utility context.
|
||||||
|
useMainWorldForSetContent?(): boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type EmulatedSize = { screen: types.Size, viewport: types.Size };
|
type EmulatedSize = { screen: types.Size, viewport: types.Size };
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import { debugLogger, type Language } from '../utils';
|
||||||
import type { Page } from './page';
|
import type { Page } from './page';
|
||||||
import { DebugController } from './debugController';
|
import { DebugController } from './debugController';
|
||||||
import type { BrowserType } from './browserType';
|
import type { BrowserType } from './browserType';
|
||||||
|
import { BidiFirefox } from './bidi/bidiFirefox';
|
||||||
|
|
||||||
type PlaywrightOptions = {
|
type PlaywrightOptions = {
|
||||||
socksProxyPort?: number;
|
socksProxyPort?: number;
|
||||||
|
|
@ -41,6 +42,7 @@ export class Playwright extends SdkObject {
|
||||||
readonly chromium: BrowserType;
|
readonly chromium: BrowserType;
|
||||||
readonly android: Android;
|
readonly android: Android;
|
||||||
readonly electron: Electron;
|
readonly electron: Electron;
|
||||||
|
readonly bidi;
|
||||||
readonly firefox: BrowserType;
|
readonly firefox: BrowserType;
|
||||||
readonly webkit: BrowserType;
|
readonly webkit: BrowserType;
|
||||||
readonly options: PlaywrightOptions;
|
readonly options: PlaywrightOptions;
|
||||||
|
|
@ -62,6 +64,7 @@ export class Playwright extends SdkObject {
|
||||||
}
|
}
|
||||||
}, null);
|
}, null);
|
||||||
this.chromium = new Chromium(this);
|
this.chromium = new Chromium(this);
|
||||||
|
this.bidi = new BidiFirefox(this);
|
||||||
this.firefox = new Firefox(this);
|
this.firefox = new Firefox(this);
|
||||||
this.webkit = new WebKit(this);
|
this.webkit = new WebKit(this);
|
||||||
this.electron = new Electron(this);
|
this.electron = new Electron(this);
|
||||||
|
|
|
||||||
|
|
@ -264,6 +264,9 @@ const DOWNLOAD_PATHS: Record<BrowserName | InternalTool, DownloadPaths> = {
|
||||||
'mac14-arm64': 'builds/android/%s/android.zip',
|
'mac14-arm64': 'builds/android/%s/android.zip',
|
||||||
'win64': 'builds/android/%s/android.zip',
|
'win64': 'builds/android/%s/android.zip',
|
||||||
},
|
},
|
||||||
|
// TODO(bidi): implement downloads.
|
||||||
|
'bidi': {
|
||||||
|
} as DownloadPaths,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const registryDirectory = (() => {
|
export const registryDirectory = (() => {
|
||||||
|
|
@ -349,14 +352,15 @@ function readDescriptors(browsersJSON: BrowsersJSON) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BrowserName = 'chromium' | 'firefox' | 'webkit';
|
export type BrowserName = 'chromium' | 'firefox' | 'webkit' | 'bidi';
|
||||||
type InternalTool = 'ffmpeg' | 'firefox-beta' | 'chromium-tip-of-tree' | 'android';
|
type InternalTool = 'ffmpeg' | 'firefox-beta' | 'chromium-tip-of-tree' | 'android';
|
||||||
|
type BidiChannel = 'bidi-firefox-stable';
|
||||||
type ChromiumChannel = 'chrome' | 'chrome-beta' | 'chrome-dev' | 'chrome-canary' | 'msedge' | 'msedge-beta' | 'msedge-dev' | 'msedge-canary';
|
type ChromiumChannel = 'chrome' | 'chrome-beta' | 'chrome-dev' | 'chrome-canary' | 'msedge' | 'msedge-beta' | 'msedge-dev' | 'msedge-canary';
|
||||||
const allDownloadable = ['chromium', 'firefox', 'webkit', 'ffmpeg', 'firefox-beta', 'chromium-tip-of-tree'];
|
const allDownloadable = ['chromium', 'firefox', 'webkit', 'ffmpeg', 'firefox-beta', 'chromium-tip-of-tree'];
|
||||||
|
|
||||||
export interface Executable {
|
export interface Executable {
|
||||||
type: 'browser' | 'tool' | 'channel';
|
type: 'browser' | 'tool' | 'channel';
|
||||||
name: BrowserName | InternalTool | ChromiumChannel;
|
name: BrowserName | InternalTool | ChromiumChannel | BidiChannel;
|
||||||
browserName: BrowserName | undefined;
|
browserName: BrowserName | undefined;
|
||||||
installType: 'download-by-default' | 'download-on-demand' | 'install-script' | 'none';
|
installType: 'download-by-default' | 'download-on-demand' | 'install-script' | 'none';
|
||||||
directory: string | undefined;
|
directory: string | undefined;
|
||||||
|
|
@ -521,6 +525,12 @@ export class Registry {
|
||||||
'win32': `\\Microsoft\\Edge SxS\\Application\\msedge.exe`,
|
'win32': `\\Microsoft\\Edge SxS\\Application\\msedge.exe`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
this._executables.push(this._createBidiChannel('bidi-firefox-stable', {
|
||||||
|
'linux': '/usr/bin/firefox',
|
||||||
|
'darwin': '/Applications/Firefox.app/Contents/MacOS/firefox',
|
||||||
|
'win32': '\\Mozilla Firefox\\firefox.exe',
|
||||||
|
}));
|
||||||
|
|
||||||
const firefox = descriptors.find(d => d.name === 'firefox')!;
|
const firefox = descriptors.find(d => d.name === 'firefox')!;
|
||||||
const firefoxExecutable = findExecutablePath(firefox.dir, 'firefox');
|
const firefoxExecutable = findExecutablePath(firefox.dir, 'firefox');
|
||||||
this._executables.push({
|
this._executables.push({
|
||||||
|
|
@ -616,6 +626,21 @@ export class Registry {
|
||||||
_dependencyGroup: 'tools',
|
_dependencyGroup: 'tools',
|
||||||
_isHermeticInstallation: true,
|
_isHermeticInstallation: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this._executables.push({
|
||||||
|
type: 'browser',
|
||||||
|
name: 'bidi',
|
||||||
|
browserName: 'bidi',
|
||||||
|
directory: undefined,
|
||||||
|
executablePath: () => undefined,
|
||||||
|
executablePathOrDie: () => '',
|
||||||
|
installType: 'none',
|
||||||
|
_validateHostRequirements: () => Promise.resolve(),
|
||||||
|
downloadURLs: [],
|
||||||
|
_install: () => Promise.resolve(),
|
||||||
|
_dependencyGroup: 'tools',
|
||||||
|
_isHermeticInstallation: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _createChromiumChannel(name: ChromiumChannel, lookAt: Record<'linux' | 'darwin' | 'win32', string>, install?: () => Promise<void>): ExecutableImpl {
|
private _createChromiumChannel(name: ChromiumChannel, lookAt: Record<'linux' | 'darwin' | 'win32', string>, install?: () => Promise<void>): ExecutableImpl {
|
||||||
|
|
@ -656,6 +681,44 @@ export class Registry {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _createBidiChannel(name: BidiChannel, lookAt: Record<'linux' | 'darwin' | 'win32', string>, install?: () => Promise<void>): ExecutableImpl {
|
||||||
|
const executablePath = (sdkLanguage: string, shouldThrow: boolean) => {
|
||||||
|
const suffix = lookAt[process.platform as 'linux' | 'darwin' | 'win32'];
|
||||||
|
if (!suffix) {
|
||||||
|
if (shouldThrow)
|
||||||
|
throw new Error(`Firefox distribution '${name}' is not supported on ${process.platform}`);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const prefixes = (process.platform === 'win32' ? [
|
||||||
|
process.env.LOCALAPPDATA, process.env.PROGRAMFILES, process.env['PROGRAMFILES(X86)']
|
||||||
|
].filter(Boolean) : ['']) as string[];
|
||||||
|
|
||||||
|
for (const prefix of prefixes) {
|
||||||
|
const executablePath = path.join(prefix, suffix);
|
||||||
|
if (canAccessFile(executablePath))
|
||||||
|
return executablePath;
|
||||||
|
}
|
||||||
|
if (!shouldThrow)
|
||||||
|
return undefined;
|
||||||
|
|
||||||
|
const location = prefixes.length ? ` at ${path.join(prefixes[0], suffix)}` : ``;
|
||||||
|
const installation = install ? `\nRun "${buildPlaywrightCLICommand(sdkLanguage, 'install ' + name)}"` : '';
|
||||||
|
throw new Error(`Firefox distribution '${name}' is not found${location}${installation}`);
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
type: 'channel',
|
||||||
|
name,
|
||||||
|
browserName: 'bidi',
|
||||||
|
directory: undefined,
|
||||||
|
executablePath: (sdkLanguage: string) => executablePath(sdkLanguage, false),
|
||||||
|
executablePathOrDie: (sdkLanguage: string) => executablePath(sdkLanguage, true)!,
|
||||||
|
installType: install ? 'install-script' : 'none',
|
||||||
|
_validateHostRequirements: () => Promise.resolve(),
|
||||||
|
_isHermeticInstallation: false,
|
||||||
|
_install: install,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
executables(): Executable[] {
|
executables(): Executable[] {
|
||||||
return this._executables;
|
return this._executables;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
packages/playwright-core/types/types.d.ts
vendored
1
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -15135,6 +15135,7 @@ export type AndroidKey =
|
||||||
|
|
||||||
export const _electron: Electron;
|
export const _electron: Electron;
|
||||||
export const _android: Android;
|
export const _android: Android;
|
||||||
|
export const _experimentalBidi: BrowserType;
|
||||||
|
|
||||||
// This is required to not export everything by default. See https://github.com/Microsoft/TypeScript/issues/19545#issuecomment-340490459
|
// This is required to not export everything by default. See https://github.com/Microsoft/TypeScript/issues/19545#issuecomment-340490459
|
||||||
export {};
|
export {};
|
||||||
|
|
|
||||||
|
|
@ -83,15 +83,15 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
||||||
options.channel = channel;
|
options.channel = channel;
|
||||||
options.tracesDir = tracing().tracesDir();
|
options.tracesDir = tracing().tracesDir();
|
||||||
|
|
||||||
for (const browserType of [playwright.chromium, playwright.firefox, playwright.webkit])
|
for (const browserType of [playwright.chromium, playwright.firefox, playwright.webkit, playwright._experimentalBidi])
|
||||||
(browserType as any)._defaultLaunchOptions = options;
|
(browserType as any)._defaultLaunchOptions = options;
|
||||||
await use(options);
|
await use(options);
|
||||||
for (const browserType of [playwright.chromium, playwright.firefox, playwright.webkit])
|
for (const browserType of [playwright.chromium, playwright.firefox, playwright.webkit, playwright._experimentalBidi])
|
||||||
(browserType as any)._defaultLaunchOptions = undefined;
|
(browserType as any)._defaultLaunchOptions = undefined;
|
||||||
}, { scope: 'worker', auto: true, box: true }],
|
}, { scope: 'worker', auto: true, box: true }],
|
||||||
|
|
||||||
browser: [async ({ playwright, browserName, _browserOptions, connectOptions, _reuseContext }, use, testInfo) => {
|
browser: [async ({ playwright, browserName, _browserOptions, connectOptions, _reuseContext }, use, testInfo) => {
|
||||||
if (!['chromium', 'firefox', 'webkit'].includes(browserName))
|
if (!['chromium', 'firefox', 'webkit', '_experimentalBidi'].includes(browserName))
|
||||||
throw new Error(`Unexpected browserName "${browserName}", must be one of "chromium", "firefox" or "webkit"`);
|
throw new Error(`Unexpected browserName "${browserName}", must be one of "chromium", "firefox" or "webkit"`);
|
||||||
|
|
||||||
if (connectOptions) {
|
if (connectOptions) {
|
||||||
|
|
|
||||||
|
|
@ -560,6 +560,7 @@ export interface RootEvents {
|
||||||
// ----------- Playwright -----------
|
// ----------- Playwright -----------
|
||||||
export type PlaywrightInitializer = {
|
export type PlaywrightInitializer = {
|
||||||
chromium: BrowserTypeChannel,
|
chromium: BrowserTypeChannel,
|
||||||
|
bidi: BrowserTypeChannel,
|
||||||
firefox: BrowserTypeChannel,
|
firefox: BrowserTypeChannel,
|
||||||
webkit: BrowserTypeChannel,
|
webkit: BrowserTypeChannel,
|
||||||
android: AndroidChannel,
|
android: AndroidChannel,
|
||||||
|
|
|
||||||
|
|
@ -668,6 +668,7 @@ Playwright:
|
||||||
|
|
||||||
initializer:
|
initializer:
|
||||||
chromium: BrowserType
|
chromium: BrowserType
|
||||||
|
bidi: BrowserType
|
||||||
firefox: BrowserType
|
firefox: BrowserType
|
||||||
webkit: BrowserType
|
webkit: BrowserType
|
||||||
android: Android
|
android: Android
|
||||||
|
|
|
||||||
95
tests/bidi/playwright.config.ts
Normal file
95
tests/bidi/playwright.config.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { config as loadEnv } from 'dotenv';
|
||||||
|
loadEnv({ path: path.join(__dirname, '..', '..', '.env'), override: true });
|
||||||
|
|
||||||
|
import { type Config, type PlaywrightTestOptions, type PlaywrightWorkerOptions, type ReporterDescription } from '@playwright/test';
|
||||||
|
import * as path from 'path';
|
||||||
|
import type { TestModeWorkerOptions } from '../config/testModeFixtures';
|
||||||
|
|
||||||
|
const getExecutablePath = () => {
|
||||||
|
return process.env.BIDIPATH;
|
||||||
|
};
|
||||||
|
|
||||||
|
const headed = process.argv.includes('--headed');
|
||||||
|
const channel = process.env.PWTEST_CHANNEL as any;
|
||||||
|
const trace = !!process.env.PWTEST_TRACE;
|
||||||
|
|
||||||
|
const outputDir = path.join(__dirname, '..', '..', 'test-results');
|
||||||
|
const testDir = path.join(__dirname, '..');
|
||||||
|
const reporters = () => {
|
||||||
|
const result: ReporterDescription[] = process.env.CI ? [
|
||||||
|
['dot'],
|
||||||
|
['json', { outputFile: path.join(outputDir, 'report.json') }],
|
||||||
|
['blob', { fileName: `${process.env.PWTEST_BOT_NAME}.zip` }],
|
||||||
|
] : [
|
||||||
|
['html', { open: 'on-failure' }]
|
||||||
|
];
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const config: Config<PlaywrightWorkerOptions & PlaywrightTestOptions & TestModeWorkerOptions> = {
|
||||||
|
testDir,
|
||||||
|
outputDir,
|
||||||
|
expect: {
|
||||||
|
timeout: 10000,
|
||||||
|
},
|
||||||
|
maxFailures: 200,
|
||||||
|
timeout: 30000,
|
||||||
|
globalTimeout: 5400000,
|
||||||
|
workers: process.env.CI ? 2 : undefined,
|
||||||
|
fullyParallel: !process.env.CI,
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
retries: process.env.CI ? 3 : 0,
|
||||||
|
reporter: reporters(),
|
||||||
|
projects: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const browserName: any = '_experimentalBidi';
|
||||||
|
const executablePath = getExecutablePath();
|
||||||
|
if (executablePath && !process.env.TEST_WORKER_INDEX)
|
||||||
|
console.error(`Using executable at ${executablePath}`);
|
||||||
|
const testIgnore: RegExp[] = [];
|
||||||
|
for (const folder of ['library', 'page']) {
|
||||||
|
config.projects.push({
|
||||||
|
name: `${browserName}-${folder}`,
|
||||||
|
testDir: path.join(testDir, folder),
|
||||||
|
testIgnore,
|
||||||
|
snapshotPathTemplate: `{testDir}/{testFileDir}/{testFileName}-snapshots/{arg}-${browserName}{ext}`,
|
||||||
|
use: {
|
||||||
|
browserName,
|
||||||
|
headless: !headed,
|
||||||
|
channel,
|
||||||
|
video: 'off',
|
||||||
|
launchOptions: {
|
||||||
|
channel: 'bidi-firefox-stable',
|
||||||
|
executablePath,
|
||||||
|
},
|
||||||
|
trace: trace ? 'on' : undefined,
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
platform: process.platform,
|
||||||
|
docker: !!process.env.INSIDE_DOCKER,
|
||||||
|
headless: !headed,
|
||||||
|
browserName,
|
||||||
|
channel,
|
||||||
|
trace: !!trace,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
|
@ -45,6 +45,7 @@ it('should scope context handles', async ({ browserType, server, expectScopeStat
|
||||||
{ _guid: 'android', objects: [] },
|
{ _guid: 'android', objects: [] },
|
||||||
{ _guid: 'browser-type', objects: [] },
|
{ _guid: 'browser-type', objects: [] },
|
||||||
{ _guid: 'browser-type', objects: [] },
|
{ _guid: 'browser-type', objects: [] },
|
||||||
|
{ _guid: 'browser-type', objects: [] },
|
||||||
{ _guid: 'browser-type', objects: [
|
{ _guid: 'browser-type', objects: [
|
||||||
{ _guid: 'browser', objects: [] }
|
{ _guid: 'browser', objects: [] }
|
||||||
] },
|
] },
|
||||||
|
|
@ -67,6 +68,7 @@ it('should scope context handles', async ({ browserType, server, expectScopeStat
|
||||||
{ _guid: 'android', objects: [] },
|
{ _guid: 'android', objects: [] },
|
||||||
{ _guid: 'browser-type', objects: [] },
|
{ _guid: 'browser-type', objects: [] },
|
||||||
{ _guid: 'browser-type', objects: [] },
|
{ _guid: 'browser-type', objects: [] },
|
||||||
|
{ _guid: 'browser-type', objects: [] },
|
||||||
{ _guid: 'browser-type', objects: [
|
{ _guid: 'browser-type', objects: [
|
||||||
{ _guid: 'browser', objects: [
|
{ _guid: 'browser', objects: [
|
||||||
{ _guid: 'browser-context', objects: [
|
{ _guid: 'browser-context', objects: [
|
||||||
|
|
@ -103,6 +105,7 @@ it('should scope CDPSession handles', async ({ browserType, browserName, expectS
|
||||||
{ _guid: 'android', objects: [] },
|
{ _guid: 'android', objects: [] },
|
||||||
{ _guid: 'browser-type', objects: [] },
|
{ _guid: 'browser-type', objects: [] },
|
||||||
{ _guid: 'browser-type', objects: [] },
|
{ _guid: 'browser-type', objects: [] },
|
||||||
|
{ _guid: 'browser-type', objects: [] },
|
||||||
{ _guid: 'browser-type', objects: [
|
{ _guid: 'browser-type', objects: [
|
||||||
{ _guid: 'browser', objects: [] }
|
{ _guid: 'browser', objects: [] }
|
||||||
] },
|
] },
|
||||||
|
|
@ -121,6 +124,7 @@ it('should scope CDPSession handles', async ({ browserType, browserName, expectS
|
||||||
{ _guid: 'android', objects: [] },
|
{ _guid: 'android', objects: [] },
|
||||||
{ _guid: 'browser-type', objects: [] },
|
{ _guid: 'browser-type', objects: [] },
|
||||||
{ _guid: 'browser-type', objects: [] },
|
{ _guid: 'browser-type', objects: [] },
|
||||||
|
{ _guid: 'browser-type', objects: [] },
|
||||||
{ _guid: 'browser-type', objects: [
|
{ _guid: 'browser-type', objects: [
|
||||||
{ _guid: 'browser', objects: [
|
{ _guid: 'browser', objects: [
|
||||||
{ _guid: 'cdp-session', objects: [] },
|
{ _guid: 'cdp-session', objects: [] },
|
||||||
|
|
@ -147,6 +151,7 @@ it('should scope browser handles', async ({ browserType, expectScopeState }) =>
|
||||||
{ _guid: 'browser-type', objects: [] },
|
{ _guid: 'browser-type', objects: [] },
|
||||||
{ _guid: 'browser-type', objects: [] },
|
{ _guid: 'browser-type', objects: [] },
|
||||||
{ _guid: 'browser-type', objects: [] },
|
{ _guid: 'browser-type', objects: [] },
|
||||||
|
{ _guid: 'browser-type', objects: [] },
|
||||||
{ _guid: 'electron', objects: [] },
|
{ _guid: 'electron', objects: [] },
|
||||||
{ _guid: 'localUtils', objects: [] },
|
{ _guid: 'localUtils', objects: [] },
|
||||||
{ _guid: 'Playwright', objects: [] },
|
{ _guid: 'Playwright', objects: [] },
|
||||||
|
|
@ -163,6 +168,7 @@ it('should scope browser handles', async ({ browserType, expectScopeState }) =>
|
||||||
{ _guid: 'android', objects: [] },
|
{ _guid: 'android', objects: [] },
|
||||||
{ _guid: 'browser-type', objects: [] },
|
{ _guid: 'browser-type', objects: [] },
|
||||||
{ _guid: 'browser-type', objects: [] },
|
{ _guid: 'browser-type', objects: [] },
|
||||||
|
{ _guid: 'browser-type', objects: [] },
|
||||||
{ _guid: 'browser-type', objects: [
|
{ _guid: 'browser-type', objects: [
|
||||||
{
|
{
|
||||||
_guid: 'browser', objects: [
|
_guid: 'browser', objects: [
|
||||||
|
|
@ -199,6 +205,7 @@ it('should not generate dispatchers for subresources w/o listeners', async ({ pa
|
||||||
{ _guid: 'android', objects: [] },
|
{ _guid: 'android', objects: [] },
|
||||||
{ _guid: 'browser-type', objects: [] },
|
{ _guid: 'browser-type', objects: [] },
|
||||||
{ _guid: 'browser-type', objects: [] },
|
{ _guid: 'browser-type', objects: [] },
|
||||||
|
{ _guid: 'browser-type', objects: [] },
|
||||||
{ _guid: 'browser-type', objects: [
|
{ _guid: 'browser-type', objects: [
|
||||||
{
|
{
|
||||||
_guid: 'browser', objects: [
|
_guid: 'browser', objects: [
|
||||||
|
|
@ -278,6 +285,10 @@ it('exposeFunction should not leak', async ({ page, expectScopeState, server })
|
||||||
'_guid': 'browser-type',
|
'_guid': 'browser-type',
|
||||||
'objects': [],
|
'objects': [],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'_guid': 'browser-type',
|
||||||
|
'objects': [],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
'_guid': 'browser-type',
|
'_guid': 'browser-type',
|
||||||
'objects': [
|
'objects': [
|
||||||
|
|
|
||||||
1
utils/generate_types/overrides.d.ts
vendored
1
utils/generate_types/overrides.d.ts
vendored
|
|
@ -377,6 +377,7 @@ export type AndroidKey =
|
||||||
|
|
||||||
export const _electron: Electron;
|
export const _electron: Electron;
|
||||||
export const _android: Android;
|
export const _android: Android;
|
||||||
|
export const _experimentalBidi: BrowserType;
|
||||||
|
|
||||||
// This is required to not export everything by default. See https://github.com/Microsoft/TypeScript/issues/19545#issuecomment-340490459
|
// This is required to not export everything by default. See https://github.com/Microsoft/TypeScript/issues/19545#issuecomment-340490459
|
||||||
export {};
|
export {};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue