Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 31 additions & 61 deletions packages/dashboard/src/dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ import './dashboard.css';
import { navigate } from './index';
import { DashboardClient } from './dashboardClient';
import { asLocator } from '@isomorphic/locatorGenerators';
import { SplitView } from '@web/components/splitView';
import { ChevronLeftIcon, ChevronRightIcon, CloseIcon, PlusIcon, ReloadIcon, PickLocatorIcon, InspectorPanelIcon } from './icons';
import { ChevronLeftIcon, ChevronRightIcon, CloseIcon, PlusIcon, ReloadIcon, PickLocatorIcon } from './icons';
import { SettingsButton } from './settingsView';

import type { DashboardClientChannel } from './dashboardClient';
Expand All @@ -43,7 +42,6 @@ export const Dashboard: React.FC<{ wsUrl?: string }> = ({ wsUrl }) => {
const [tabs, setTabs] = React.useState<Tab[] | null>(null);
const [url, setUrl] = React.useState('');
const [frame, setFrame] = React.useState<DashboardChannelEvents['frame']>();
const [showInspector, setShowInspector] = React.useState(false);
const [pickingTabId, setPickingTabId] = React.useState<string | null>(null);
const [locatorToast, setLocatorToast] = React.useState<{ text: string; timer: ReturnType<typeof setTimeout> }>();

Expand Down Expand Up @@ -103,7 +101,6 @@ export const Dashboard: React.FC<{ wsUrl?: string }> = ({ wsUrl }) => {
setChannel(undefined);
setInteractive(false);
setPickingTabId(null);
setShowInspector(false);
};

return () => {
Expand Down Expand Up @@ -272,7 +269,6 @@ export const Dashboard: React.FC<{ wsUrl?: string }> = ({ wsUrl }) => {
onClick={() => {
channel?.cancelPickLocator();
setPickingTabId(null);
setShowInspector(false);
setInteractive(false);
}}
>
Expand Down Expand Up @@ -334,66 +330,40 @@ export const Dashboard: React.FC<{ wsUrl?: string }> = ({ wsUrl }) => {
>
<PickLocatorIcon />
</button>
{selectedTab?.inspectorUrl && (
<button
className={'nav-btn' + (showInspector ? ' active-toggle' : '')}
title='Chrome DevTools'
aria-pressed={showInspector}
disabled={!channel}
onClick={() => {
setInteractive(true);
setShowInspector(!showInspector);
}}
>
<InspectorPanelIcon />
</button>
)}
</div>

{/* Viewport */}
<div className='viewport-wrapper'>
<SplitView
orientation='horizontal'
sidebarSize={500}
minSidebarSize={300}
settingName='devtoolsInspector'
sidebarHidden={!showInspector || !selectedTab?.inspectorUrl}
main={<div className='viewport-main'>
<div
ref={screenRef}
className='screen'
tabIndex={0}
style={{ display: frame ? '' : 'none' }}
onMouseDown={onScreenMouseDown}
onMouseUp={onScreenMouseUp}
onMouseMove={onScreenMouseMove}
onWheel={onScreenWheel}
onKeyDown={onScreenKeyDown}
onKeyUp={onScreenKeyUp}
onContextMenu={e => e.preventDefault()}
>
<img
ref={displayRef}
id='display'
className='display'
alt='screencast'
src={frame ? 'data:image/jpeg;base64,' + frame.data : undefined}
/>
{locatorToast
? <div className='screen-toast visible'>Copied: <code>{locatorToast.text}</code></div>
: picking
? <div className='screen-toast visible'>Click an element to pick its locator</div>
: null
}
</div>
{overlayText && <div className={'screen-overlay' + (frame ? ' has-frame' : '')}><span>{overlayText}</span></div>}
</div>}
sidebar={<iframe
className='inspector-frame'
src={selectedTab?.inspectorUrl || ''}
title='Chrome DevTools'
/>}
/>
<div className='viewport-main'>
<div
ref={screenRef}
className='screen'
tabIndex={0}
style={{ display: frame ? '' : 'none' }}
onMouseDown={onScreenMouseDown}
onMouseUp={onScreenMouseUp}
onMouseMove={onScreenMouseMove}
onWheel={onScreenWheel}
onKeyDown={onScreenKeyDown}
onKeyUp={onScreenKeyUp}
onContextMenu={e => e.preventDefault()}
>
<img
ref={displayRef}
id='display'
className='display'
alt='screencast'
src={frame ? 'data:image/jpeg;base64,' + frame.data : undefined}
/>
{locatorToast
? <div className='screen-toast visible'>Copied: <code>{locatorToast.text}</code></div>
: picking
? <div className='screen-toast visible'>Click an element to pick its locator</div>
: null
}
</div>
{overlayText && <div className={'screen-overlay' + (frame ? ' has-frame' : '')}><span>{overlayText}</span></div>}
</div>
</div>
</div>);
};
2 changes: 1 addition & 1 deletion packages/dashboard/src/dashboardChannel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

export type Tab = { pageId: string; title: string; url: string; selected: boolean; inspectorUrl?: string };
export type Tab = { pageId: string; title: string; url: string; selected: boolean };

export type DashboardChannelEvents = {
frame: { data: string; viewportWidth: number; viewportHeight: number };
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright-core/src/server/android/android.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ export class AndroidDevice extends SdkObject {
'--disable-fre',
'--no-default-browser-check',
`--remote-debugging-socket-name=${socketName}`,
...chromiumSwitches(undefined, undefined, true),
...chromiumSwitches({ android: true }),
...this._innerDefaultArgs(options)
];
return chromeArguments;
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright-core/src/server/bidi/bidiChromium.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export class BidiChromium extends BrowserType {
throw new Error('Playwright manages remote debugging connection itself.');
if (args.find(arg => !arg.startsWith('-')))
throw new Error('Arguments can not specify page to be opened');
const chromeArguments = [...chromiumSwitches(options.assistantMode)];
const chromeArguments = [...chromiumSwitches()];

if (os.platform() === 'darwin') {
// See https://issues.chromium.org/issues/40277080
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright-core/src/server/chromium/chromium.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ export class Chromium extends BrowserType {
throw new Error('Playwright manages remote debugging connection itself.');
if (args.find(arg => !arg.startsWith('-')))
throw new Error('Arguments can not specify page to be opened');
const chromeArguments = [...chromiumSwitches(options.assistantMode, options.channel)];
const chromeArguments = [...chromiumSwitches()];

// See https://issues.chromium.org/issues/40277080
chromeArguments.push('--enable-unsafe-swiftshader');
Expand Down
10 changes: 4 additions & 6 deletions packages/playwright-core/src/server/chromium/chromiumSwitches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

// No dependencies as it is used from the Electron loader.

const disabledFeatures = (assistantMode?: boolean) => [
const disabledFeatures = [
// See https://github.com/microsoft/playwright/issues/14047
'AvoidUnnecessaryBeforeUnloadCheckSync',
// See https://github.com/microsoft/playwright/issues/38568
Expand All @@ -44,14 +44,13 @@ const disabledFeatures = (assistantMode?: boolean) => [
'RenderDocument',
// Prevents downloading optimization hints on startup.
'OptimizationHints',
assistantMode ? 'AutomationControlled' : '',
// Disables forced sign-in in Edge.
'msForceBrowserSignIn',
// Disables updating the preferred version in LaunchServices preferences on mac.
'msEdgeUpdateLaunchServicesPreferredVersion',
].filter(Boolean);

export const chromiumSwitches = (assistantMode?: boolean, channel?: string, android?: boolean) => [
export const chromiumSwitches = (options?: { android?: boolean }) => [
'--disable-field-trial-config', // https://source.chromium.org/chromium/chromium/src/+/main:testing/variations/README.md
'--disable-background-networking',
'--disable-background-timer-throttling',
Expand All @@ -66,7 +65,7 @@ export const chromiumSwitches = (assistantMode?: boolean, channel?: string, andr
'--disable-dev-shm-usage',
'--disable-edgeupdater', // Disables Edge-specific updater on mac.
'--disable-extensions',
'--disable-features=' + disabledFeatures(assistantMode).join(','),
'--disable-features=' + disabledFeatures.join(','),
process.env.PLAYWRIGHT_LEGACY_SCREENSHOT ? '' : '--enable-features=CDPScreenshotNewSurface',
'--allow-pre-commit-input',
'--disable-hang-monitor',
Expand All @@ -88,12 +87,11 @@ export const chromiumSwitches = (assistantMode?: boolean, channel?: string, andr
'--unsafely-disable-devtools-self-xss-warnings',
// Edge can potentially restart on Windows (msRelaunchNoCompatLayer) which looses its file descriptors (stdout/stderr) and CDP (3/4). Disable until fixed upstream.
'--edge-skip-compat-layer-relaunch',
assistantMode ? '' : '--enable-automation',
// This disables Chrome for Testing infobar that is visible in the persistent context.
// The switch is ignored everywhere else, including Chromium/Chrome/Edge.
'--disable-infobars',
// Less annoying popups.
'--disable-search-engine-choice-screen',
// Prevents the "three dots" menu crash in IdentityManager::HasPrimaryAccount for ephemeral contexts.
android ? '' : '--disable-sync',
options?.android ? '' : '--disable-sync',
].filter(Boolean);
18 changes: 2 additions & 16 deletions packages/playwright-core/src/tools/dashboard/dashboardApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { gracefullyProcessExitDoNotHang } from '@utils/processLauncher';
import { libPath } from '../../package';
import { playwright } from '../../inprocess';
import { findChromiumChannelBestEffort, registryDirectory } from '../../server/registry/index';
import { CDPConnection, DashboardConnection } from './dashboardController';
import { DashboardConnection } from './dashboardController';
import { serverRegistry } from '../../serverRegistry';
import { connectToBrowserAcrossVersions } from '../utils/connect';
import { createClientInfo } from '../cli-client/registry';
Expand Down Expand Up @@ -147,21 +147,7 @@ async function innerOpenDashboardApp(): Promise<api.Page> {
throw new Error('Unsupported WebSocket URL: ' + url.toString());
const browserDescriptor = serverRegistry.readDescriptor(guid);

const cdpPageId = url.searchParams.get('cdpPageId');
if (cdpPageId) {
const connection = browserGuidToDashboardConnection.get(guid);
if (!connection)
throw new Error('CDP connection not found for session: ' + guid);
const page = connection.pageForId(cdpPageId);
if (!page)
throw new Error('Page not found for page ID: ' + cdpPageId);
return new CDPConnection(page);
}

const cdpUrl = new URL(httpServer.urlPrefix('human-readable'));
cdpUrl.pathname = httpServer.wsGuid()!;
cdpUrl.searchParams.set('guid', guid);
const connection = new DashboardConnection(browserDescriptor, cdpUrl, () => browserGuidToDashboardConnection.delete(guid));
const connection = new DashboardConnection(browserDescriptor, () => browserGuidToDashboardConnection.delete(guid));
browserGuidToDashboardConnection.set(guid, connection);
return connection;
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,14 @@ export class DashboardConnection implements Transport, DashboardChannel {
private _eventListeners = new Map<string, Set<Function>>();

private _browserDescriptor: BrowserDescriptor;
private _cdpUrl: URL;
private _onclose: () => void;

private _initPromise?: Promise<void>;
private _context!: api.BrowserContext;
private _browser?: api.Browser;

constructor(browserDescriptor: BrowserDescriptor, cdpUrl: URL, onclose: () => void) {
constructor(browserDescriptor: BrowserDescriptor, onclose: () => void) {
this._browserDescriptor = browserDescriptor;
this._cdpUrl = cdpUrl;
this._onclose = onclose;
}

Expand Down Expand Up @@ -242,51 +240,22 @@ export class DashboardConnection implements Transport, DashboardChannel {

private async _tabList(): Promise<Tab[]> {
const pages = this._context.pages();
if (pages.length === 0)
return [];
const devtoolsUrl = await this._devtoolsUrl(pages[0]);
return await Promise.all(pages.map(async page => {
const title = await page.title();
return {
pageId: this._pageId(page),
title,
url: page.url(),
selected: page === this.selectedPage,
inspectorUrl: devtoolsUrl ? await this._pageInspectorUrl(page, devtoolsUrl) : 'data:text/plain,Dashboard only supported in Chromium based browsers',
};
}));
}

pageForId(pageId: string) {
return this._context?.pages().find(p => this._pageId(p) === pageId);
}

private _pageId(p: api.Page): string {
// eslint-disable-next-line no-restricted-syntax -- _guid is very conservative.
return (p as any)._guid;
}

private async _devtoolsUrl(page: api.Page) {
// eslint-disable-next-line no-restricted-syntax -- cdpPort is not in the public LaunchOptions type, fine if regresses.
const cdpPort = (this._browserDescriptor.browser.launchOptions as any).cdpPort;
if (cdpPort)
return new URL(`http://localhost:${cdpPort}/devtools/`);

const browserRevision = await getBrowserRevision(page);
if (!browserRevision)
return null;
return new URL(`https://chrome-devtools-frontend.appspot.com/serve_rev/${browserRevision}/`);
}

private async _pageInspectorUrl(page: api.Page, devtoolsUrl: URL): Promise<string | undefined> {
const inspector = new URL('./devtools_app.html', devtoolsUrl);
const cdp = new URL(this._cdpUrl);
cdp.searchParams.set('cdpPageId', this._pageId(page));
inspector.searchParams.set('ws', `${cdp.host}${cdp.pathname}${cdp.search}`);
const url = inspector.toString();
return url;
}

private _sendTabList() {
this._tabList().then(tabs => this._emit('tabs', { tabs }));
}
Expand All @@ -298,59 +267,3 @@ export class DashboardConnection implements Transport, DashboardChannel {
this._emit('frame', { data, viewportWidth, viewportHeight });
}
}

async function getBrowserRevision(page: api.Page): Promise<string | null> {
try {
const session = await page.context().newCDPSession(page);
const version = await session.send('Browser.getVersion');
await session.detach();
return version.revision;
} catch (error) {
return null;
}
}

export class CDPConnection implements Transport {
sendEvent?: (method: string, params: any) => void;
close?: () => void;

private _page: api.Page;
private _rawSession: api.CDPSession | null = null;
private _rawSessionListeners: { dispose: () => Promise<void> }[] = [];
private _initializePromise: Promise<void> | undefined;

constructor(page: api.Page) {
this._page = page;
}

onconnect() {
this._initializePromise = this._initializeRawSession();
}

async dispatch(method: string, params: any): Promise<any> {
await this._initializePromise;
if (!this._rawSession)
throw new Error('CDP session is not initialized');
return await this._rawSession.send(method as Parameters<api.CDPSession['send']>[0], params);
}

onclose() {
this._rawSessionListeners.forEach(listener => listener.dispose());
this._rawSession?.detach().catch(() => {});
this._rawSession = null;
this._initializePromise = undefined;
}

private async _initializeRawSession() {
const session = await this._page.context().newCDPSession(this._page);
this._rawSession = session;
this._rawSessionListeners = [
eventsHelper.addEventListener(session, 'event', ({ method, params }) => {
this.sendEvent?.(method, params);
}),
eventsHelper.addEventListener(session, 'close', () => {
this.close?.();
}),
];
}
}
Loading
Loading